├── .npmrc ├── .eslintignore ├── versions.json ├── .editorconfig ├── .prettierrc ├── manifest.json ├── .gitignore ├── tsconfig.json ├── version-bump.mjs ├── .eslintrc ├── src ├── icons.ts ├── modals.ts ├── types.ts ├── settings.ts ├── view.tsx ├── components │ ├── renderer │ │ └── index.tsx │ └── search │ │ └── index.tsx ├── main.ts ├── database.ts ├── utils.ts └── modules │ └── msgreader.js ├── LICENSE.md ├── esbuild.config.mjs ├── package.json ├── styles.css └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | tag-version-prefix="" -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | main.js 4 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "0.0.6": "0.15.0", 3 | "0.0.5": "0.15.0", 4 | "0.0.4": "0.15.0", 5 | "0.0.3": "0.15.0", 6 | "0.0.2": "0.15.0", 7 | "0.0.1": "0.15.0", 8 | "0.0.0": "0.15.0" 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | insert_final_newline = true 8 | indent_style = tab 9 | indent_size = 4 10 | tab_width = 4 11 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "singleQuote": true, 6 | "printWidth": 115, 7 | "jsxBracketSameLine": true, 8 | "bracketSpacing": true, 9 | "arrowParens": "always" 10 | } 11 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "msg-handler", 3 | "name": "MSG Handler", 4 | "version": "0.0.6", 5 | "minAppVersion": "0.15.0", 6 | "description": "Easily display and search MSG files from Outlook in your Obsidian Vault", 7 | "author": "Ozan Tellioglu", 8 | "authorUrl": "https://www.ozan.pl", 9 | "fundingUrl": "https://ko-fi.com/ozante", 10 | "isDesktopOnly": false 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # vscode 2 | .vscode 3 | 4 | # Intellij 5 | *.iml 6 | .idea 7 | 8 | # npm 9 | node_modules 10 | 11 | # Don't include the compiled main.js file in the repo. 12 | # They should be uploaded to GitHub releases instead. 13 | main.js 14 | 15 | # Exclude sourcemaps 16 | *.map 17 | 18 | # obsidian 19 | data.json 20 | 21 | # Exclude macOS Finder (System Explorer) View States 22 | .DS_Store 23 | 24 | package-lock.json -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "esModuleInterop": true, 5 | "baseUrl": "src", 6 | "inlineSourceMap": true, 7 | "inlineSources": true, 8 | "isolatedModules": true, 9 | "module": "ESNext", 10 | "target": "es6", 11 | "allowJs": true, 12 | "noImplicitAny": true, 13 | "moduleResolution": "node", 14 | "importHelpers": true, 15 | "allowSyntheticDefaultImports": true, 16 | "lib": ["dom", "es5", "scripthost", "es2015"] 17 | }, 18 | "include": ["**/*.ts", "**/*.tsx", "src/view.tsx"] 19 | } 20 | -------------------------------------------------------------------------------- /version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const targetVersion = process.env.npm_package_version; 4 | 5 | // read minAppVersion from manifest.json and bump version to target version 6 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | const { minAppVersion } = manifest; 8 | manifest.version = targetVersion; 9 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 10 | 11 | // update versions.json with target version and minAppVersion from manifest.json 12 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 13 | versions[targetVersion] = minAppVersion; 14 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 15 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "env": { "node": true }, 5 | "plugins": [ 6 | "@typescript-eslint" 7 | ], 8 | "extends": [ 9 | "eslint:recommended", 10 | "plugin:@typescript-eslint/eslint-recommended", 11 | "plugin:@typescript-eslint/recommended" 12 | ], 13 | "parserOptions": { 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-unused-vars": "off", 18 | "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], 19 | "@typescript-eslint/ban-ts-comment": "off", 20 | "no-prototype-builtins": "off", 21 | "@typescript-eslint/no-empty-function": "off" 22 | } 23 | } -------------------------------------------------------------------------------- /src/icons.ts: -------------------------------------------------------------------------------- 1 | export const MSG_HANDLER_ENVELOPE_ICON = ``; 2 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 Ozan Tellioglu. 2 | 3 | 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: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | 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. 8 | -------------------------------------------------------------------------------- /esbuild.config.mjs: -------------------------------------------------------------------------------- 1 | import esbuild from "esbuild"; 2 | import process from "process"; 3 | import builtins from "builtin-modules"; 4 | 5 | const banner = `/* 6 | THIS IS A GENERATED/BUNDLED FILE BY ESBUILD 7 | if you want to view the source, please visit the github repository of this plugin 8 | */ 9 | `; 10 | 11 | const prod = process.argv[2] === "production"; 12 | 13 | const context = await esbuild.context({ 14 | banner: { 15 | js: banner, 16 | }, 17 | entryPoints: ["src/main.ts"], 18 | bundle: true, 19 | external: [ 20 | "obsidian", 21 | "electron", 22 | "@codemirror/autocomplete", 23 | "@codemirror/collab", 24 | "@codemirror/commands", 25 | "@codemirror/language", 26 | "@codemirror/lint", 27 | "@codemirror/search", 28 | "@codemirror/state", 29 | "@codemirror/view", 30 | "@lezer/common", 31 | "@lezer/highlight", 32 | "@lezer/lr", 33 | ...builtins, 34 | ], 35 | format: "cjs", 36 | target: "es2018", 37 | logLevel: "info", 38 | sourcemap: prod ? false : "inline", 39 | treeShaking: true, 40 | outfile: "main.js", 41 | }); 42 | 43 | if (prod) { 44 | await context.rebuild(); 45 | process.exit(0); 46 | } else { 47 | await context.watch(); 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "msg-handler", 3 | "version": "0.0.6", 4 | "description": "This plugin is created to display and easily search for msg files from Outlook", 5 | "main": "main.js", 6 | "scripts": { 7 | "dev": "node esbuild.config.mjs", 8 | "build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production", 9 | "version": "node version-bump.mjs && git add manifest.json versions.json" 10 | }, 11 | "keywords": [], 12 | "author": "", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/common-tags": "^1.8.1", 16 | "@types/node": "^16.11.6", 17 | "@types/react": "17.0.2", 18 | "@types/react-dom": "17.0.2", 19 | "@types/showdown": "^2.0.0", 20 | "@typescript-eslint/eslint-plugin": "5.29.0", 21 | "@typescript-eslint/parser": "5.29.0", 22 | "builtin-modules": "3.3.0", 23 | "esbuild": "0.17.3", 24 | "obsidian": "latest", 25 | "tslib": "2.4.0", 26 | "typescript": "4.7.4" 27 | }, 28 | "dependencies": { 29 | "common-tags": "^1.8.2", 30 | "dayjs": "^1.11.7", 31 | "dexie": "^3.2.3", 32 | "eml-parse-js": "^1.1.8", 33 | "fuzzysort": "^2.0.4", 34 | "js-base64": "^3.7.5", 35 | "preact": "^10", 36 | "prettier": "^2.8.4", 37 | "react": "npm:@preact/compat@17.0.2", 38 | "react-dom": "npm:@preact/compat@17.0.2", 39 | "react-icons": "^4.7.1" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/modals.ts: -------------------------------------------------------------------------------- 1 | import { FuzzySuggestModal, TFolder, App } from 'obsidian'; 2 | import { base64ToArrayBuffer } from './utils'; 3 | 4 | export class FolderToSaveSuggestionModal extends FuzzySuggestModal { 5 | app: App; 6 | fileName: string; 7 | fileToSave: Uint8Array; 8 | 9 | constructor(app: App, fileToSave: Uint8Array | string, fileName: string) { 10 | super(app); 11 | this.fileName = fileName; 12 | if (typeof fileToSave === 'string') { 13 | fileToSave = base64ToArrayBuffer(fileToSave); 14 | } 15 | this.fileToSave = fileToSave; 16 | } 17 | 18 | getItemText(item: TFolder): string { 19 | return item.path; 20 | } 21 | 22 | getItems(): TFolder[] { 23 | return getAllFoldersInVault(this.app); 24 | } 25 | 26 | onChooseItem(item: TFolder, evt: MouseEvent | KeyboardEvent) { 27 | this.app.vault.createBinary(item.path + '/' + this.fileName, this.fileToSave); 28 | } 29 | } 30 | 31 | function getAllFoldersInVault(app: App): TFolder[] { 32 | let folders: TFolder[] = []; 33 | let rootFolder = app.vault.getRoot(); 34 | folders.push(rootFolder); 35 | function recursiveFx(folder: TFolder) { 36 | for (let child of folder.children) { 37 | if (child instanceof TFolder) { 38 | let childFolder: TFolder = child as TFolder; 39 | folders.push(childFolder); 40 | if (childFolder.children) recursiveFx(childFolder); 41 | } 42 | } 43 | } 44 | recursiveFx(rootFolder); 45 | return folders; 46 | } 47 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface MSGBaseData { 2 | senderName: string; 3 | senderEmail: string; 4 | recipients: MSGRecipient[]; 5 | creationTime: string; 6 | subject: string; 7 | body: string; 8 | } 9 | 10 | // --> This type is created to use for direct render for renderer/index 11 | export interface MSGRenderData extends MSGBaseData { 12 | attachments: MSGAttachment[]; 13 | } 14 | 15 | // --> Message Recipient Details 16 | export interface MSGRecipient { 17 | name: string; 18 | email: string; 19 | } 20 | 21 | // --> Message Attachment details 22 | export interface MSGAttachment { 23 | fileName: string; 24 | fileExtension: string; 25 | fileBase64: string; 26 | } 27 | 28 | // --> This type is created to store indexed data within the database 29 | export interface MSGDataIndexed extends MSGBaseData { 30 | id?: number; 31 | filePath: string; 32 | mtime: number; 33 | } 34 | 35 | // --> This type is only used for the purpose of fussysort search results 36 | // To ensure that all object fields are searched only after converted into 37 | // eligible string 38 | export interface MSGDataIndexedSearchEligible extends Omit { 39 | recipients: string; 40 | } 41 | 42 | /* --- External Library Helper Interface --- */ 43 | 44 | export interface Ext_MSGReader_FileData { 45 | senderName: string; 46 | senderEmail: string; 47 | recipients: Ext_MSGReader_Recipient[]; 48 | subject: string; 49 | body: string; 50 | attachments: Ext_MSGReader_Attachment[]; 51 | // Not used 52 | headers: string; 53 | bodyHTML: string; 54 | } 55 | 56 | export interface Ext_MSGReader_Recipient { 57 | name: string; 58 | email: string; 59 | } 60 | 61 | export interface Ext_MSGReader_Attachment { 62 | contentLenght: number; 63 | dataId: number; 64 | extension: string; 65 | fileName: string; 66 | fileNameShort: string; 67 | mimeType: string; 68 | name: string; 69 | pidContentId: string; 70 | } 71 | 72 | export interface Ext_MSGReader_AttachmentData { 73 | fileName: string; 74 | content: Uint8Array; 75 | } 76 | -------------------------------------------------------------------------------- /src/settings.ts: -------------------------------------------------------------------------------- 1 | import MsgHandlerPlugin from 'main'; 2 | import { PluginSettingTab, Setting, App } from 'obsidian'; 3 | 4 | export interface MSGHandlerPluginSettings { 5 | searchEnabled: boolean; 6 | logEnabled: boolean; 7 | } 8 | 9 | export const DEFAULT_SETTINGS: MSGHandlerPluginSettings = { 10 | searchEnabled: true, 11 | logEnabled: false, 12 | }; 13 | 14 | export class MSGHandlerPluginSettingsTab extends PluginSettingTab { 15 | plugin: MsgHandlerPlugin; 16 | 17 | constructor(app: App, plugin: MsgHandlerPlugin) { 18 | super(app, plugin); 19 | this.plugin = plugin; 20 | } 21 | 22 | display(): void { 23 | let { containerEl } = this; 24 | 25 | const tipDiv = containerEl.createDiv('tip'); 26 | tipDiv.addClass('oz-msg-handler-tip-div'); 27 | const tipLink = tipDiv.createEl('a', { href: 'https://revolut.me/ozante' }); 28 | const tipImg = tipLink.createEl('img', { 29 | attr: { 30 | src: 'https://raw.githubusercontent.com/ozntel/file-tree-alternative/main/images/tip%20the%20artist_v2.png', 31 | }, 32 | }); 33 | tipImg.height = 55; 34 | 35 | const coffeeDiv = containerEl.createDiv('coffee'); 36 | coffeeDiv.addClass('oz-msg-handler-coffee-div'); 37 | const coffeeLink = coffeeDiv.createEl('a', { href: 'https://ko-fi.com/L3L356V6Q' }); 38 | const coffeeImg = coffeeLink.createEl('img', { 39 | attr: { 40 | src: 'https://cdn.ko-fi.com/cdn/kofi2.png?v=3', 41 | }, 42 | }); 43 | coffeeImg.height = 45; 44 | 45 | /* ------------- Header ------------- */ 46 | 47 | let headerSettings = containerEl.createEl('h1'); 48 | headerSettings.innerText = 'MSG Handler Settings'; 49 | 50 | /* ------------- General Settings ------------- */ 51 | 52 | new Setting(containerEl) 53 | .setName('MSG Search View') 54 | .setDesc('Turn on if you want automatically to be opened Search View after each vault/plugin refresh') 55 | .addToggle((toggle) => 56 | toggle.setValue(this.plugin.settings.searchEnabled).onChange((value) => { 57 | this.plugin.settings.searchEnabled = value; 58 | this.plugin.saveSettings(); 59 | if (value) { 60 | this.plugin.openMsgHandlerSearchLeaf({ showAfterAttach: true }); 61 | } else { 62 | this.plugin.detachMsgHandlerSearchLeaf(); 63 | } 64 | }) 65 | ); 66 | 67 | new Setting(containerEl) 68 | .setName('Plugin Logs in Console') 69 | .setDesc('Turn on if you want to see the plugin logs within the console for actions') 70 | .addToggle((toggle) => 71 | toggle.setValue(this.plugin.settings.logEnabled).onChange((value) => { 72 | this.plugin.settings.logEnabled = value; 73 | this.plugin.saveSettings(); 74 | }) 75 | ); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .workspace-leaf-content[data-type='msg-handler-view'] { 2 | user-select: text; 3 | padding-left: 5px; 4 | padding-right: 5px; 5 | padding-top: 5px; 6 | } 7 | 8 | .oz-msg-handler-body, 9 | .oz-msg-handler-header, 10 | .oz-msg-handler-attachments { 11 | padding: 14px; 12 | width: 100%; 13 | border-radius: 7px; 14 | background-color: var(--background-secondary); 15 | } 16 | 17 | .msg-handler-plugin-search .search-result-file-matches { 18 | padding: 5px; 19 | } 20 | 21 | .msg-handler-plugin-search .search-result-container { 22 | padding-left: 0px; 23 | padding-right: 0px; 24 | } 25 | 26 | .oz-msg-handler-body, 27 | .oz-msg-handler-header { 28 | margin-bottom: 10px; 29 | } 30 | 31 | .workspace-leaf-content[data-type='msg-handler-view'] .external-link { 32 | text-decoration: none; 33 | color: var(--text-muted); 34 | } 35 | 36 | .workspace-leaf-content[data-type='msg-handler-view'] p { 37 | margin: 5px 0px 5px 0px; 38 | } 39 | 40 | .MSG_HANDLER_ENVELOPE_ICON { 41 | fill: var(--icon-color) !important; 42 | } 43 | 44 | .oz-highlight { 45 | background-color: yellow; 46 | border: 0.5px solid black; 47 | } 48 | 49 | .oz-searchbox-container, 50 | .oz-searchbox-container input { 51 | width: 100%; 52 | } 53 | 54 | .oz-msg-handler-actions-items { 55 | text-align: center; 56 | padding: 3px 2px 0px 2px; 57 | background-color: var(--background-secondary-alt); 58 | margin-bottom: 7px; 59 | border-radius: 5px; 60 | } 61 | 62 | .oz-msg-handler-header-fixed { 63 | position: sticky; 64 | top: 0; 65 | padding-left: 4px; 66 | padding-right: 4px; 67 | z-index: 100; 68 | } 69 | 70 | .oz-msg-handler-action-button { 71 | color: var(--text-muted); 72 | display: inline-block; 73 | padding: 0px 0px 0px 2px; 74 | margin-left: 5px; 75 | opacity: 0.5; 76 | border-radius: 8px; 77 | opacity: 1; 78 | } 79 | 80 | .oz-msg-handler-action-button:hover { 81 | opacity: 0.6; 82 | } 83 | 84 | .oz-msg-handler-action-button svg { 85 | vertical-align: middle !important; 86 | } 87 | 88 | .msg-handler-react-icon { 89 | vertical-align: middle !important; 90 | padding-bottom: 2px; 91 | } 92 | 93 | .msg-handler-react-icon:hover { 94 | opacity: 0.6; 95 | cursor: pointer; 96 | } 97 | 98 | .workspace-tab-header.is-active[aria-label='MSG Handler Search'] svg { 99 | fill: var(--icon-color-focused); 100 | } 101 | 102 | .oz-cursor-pointer { 103 | cursor: pointer; 104 | } 105 | 106 | .oz-attachment-display { 107 | margin-top: 10px; 108 | } 109 | 110 | .oz-msg-attachment-name button { 111 | cursor: pointer; 112 | margin-left: 14px; 113 | padding-top: 0px; 114 | padding-bottom: 0px; 115 | } 116 | 117 | .oz-msg-single-attachment-wrapper { 118 | margin-top: 3px; 119 | } 120 | 121 | .oz-msg-header-name, 122 | .oz-msg-attachments-header-name, 123 | .oz-msg-attachments-body-name { 124 | color: var(--text-muted); 125 | } 126 | 127 | .oz-msg-handler-coffee-div, 128 | .oz-msg-handler-tip-div { 129 | text-align: center; 130 | margin-top: 10px; 131 | } 132 | 133 | .oz-msg-handler-tip-div img { 134 | border-radius: 10px; 135 | } 136 | 137 | .oz-msg-handler-preview-render { 138 | max-height: 400px; 139 | overflow: scroll; 140 | border-left: var(--embed-border-left); 141 | padding: var(--embed-padding); 142 | } 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Obsidian MSG and EML Handler Plugin 2 | 3 | Obsidian by default doesn't open any **Outlook** (`.msg`, `.eml`) files. It will prompt you to open the file in your OS's default application. This plugin is created to **Display** and **Easily Search** for Outlook items you save within your folder. 4 | 5 | Most companies have a retention policy when it comes to emails (like 2, or 3 years). It might be even shorter. You will need to save your important emails on your computer. Or you might want to save and search only for particular Outlook messages even if you don't have any retention policy. This plugin comes in handy for such people to easily find relevant items and open them. 6 | 7 | The plugin basically adds a custom view to handle files with `.msg` and `.eml` extensions. There is an additional **Search View** created to find what you are searching for easily. It looks very identical to Obsidian's default searcher since it is using the same style classes to make it easier for users to use at any time. 8 | 9 | To make the search functionality faster, the plugin observes your vault changes when it comes to `.msg` and `.eml` files and indexes them within a database so that it doesn't need to go back to the file and read it for each search. After each vault open/plugin load, the plugin will cross check all `.msg` and `.eml` files within your vault vs the database and make the necessary updates just in case you brought some of the the `.msg` or `.eml` files when the plugin was not turned on or your vault was not open. 10 | 11 | In the plugin msg file view, you will have 3 sections: 12 | 13 | - **Header**: Includes information like sender name, sender email, recipients name and email, subject 14 | - **Body** : Includes the plain text version of email body 15 | - **Attachments**: Includes the attachments of the email. The plugin will render the images and hide them automatically by using a toggle button. You can toggle to see them. If the file is not an image, you can save the file in your vault in any folder you want. The plugin will prompt you to select the folder to save. 16 | 17 | ## View Messages in Editor Source Mode 18 | 19 | You can install **Ozan's Image in Editor** plugin to view the embedded preview of your `.msg` or `.eml` files directly from the editor using WikiLinks: 20 | 21 | ```md 22 | ![[MyMessageFromOutlook.msg]] 23 | ![[AnotherMessageToSee.eml]] 24 | ``` 25 | 26 | Make sure that you enable rendering msg files from the **Ozan's Image in Editor** plugin settings. 27 | 28 | ## View Messages in Preview Mode 29 | 30 | The plugin by default supports the preview of embedded images in Obsidian's Preview Mode. If you are using Editor Source Mode combined with Preview mode, your embedded messages are always going to be displayed along with your markdown note. Same like Editor Source Mode, use the Wikilink format. 31 | 32 | ## Contact 33 | 34 | If you have any issue or you have any suggestion, please feel free to reach me out directly using contact page of my website [ozan.pl/contact/](https://www.ozan.pl/contact/) or directly to . 35 | 36 | ## Support 37 | 38 | If you are enjoying the plugin then you can support my work and enthusiasm by buying me a coffee: 39 | 40 | 41 | Buy Me a Coffee at ko-fi.com 42 | 43 | -------------------------------------------------------------------------------- /src/view.tsx: -------------------------------------------------------------------------------- 1 | import { FileView, TFile, WorkspaceLeaf, ItemView } from 'obsidian'; 2 | import MsgHandlerPlugin from 'main'; 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import SearchViewComponent from 'components/search'; 6 | import RendererViewComponent from 'components/renderer'; 7 | 8 | /* ------------ CORE MSG HANDLER RENDERER WITH REACT ------------ */ 9 | 10 | export const renderMsgFileToElement = async (params: { 11 | msgFile: TFile; 12 | targetEl: HTMLElement; 13 | plugin: MsgHandlerPlugin; 14 | }) => { 15 | const { msgFile, targetEl, plugin } = params; 16 | return new Promise((resolve, reject) => { 17 | ReactDOM.render( 18 |
19 | 20 |
, 21 | targetEl, 22 | () => resolve() 23 | ); 24 | }); 25 | }; 26 | 27 | /* ------------ RENDERER VIEW FOR FILE PREVIEW ------------ */ 28 | 29 | export const RENDER_VIEW_TYPE = 'msg-handler-view'; 30 | 31 | export class MsgHandlerView extends FileView { 32 | plugin: MsgHandlerPlugin; 33 | fileToRender: TFile; 34 | 35 | constructor(leaf: WorkspaceLeaf, plugin: MsgHandlerPlugin) { 36 | super(leaf); 37 | this.plugin = plugin; 38 | } 39 | 40 | getViewType(): string { 41 | return RENDER_VIEW_TYPE; 42 | } 43 | 44 | destroy() { 45 | ReactDOM.unmountComponentAtNode(this.contentEl); 46 | } 47 | 48 | async onLoadFile(file: TFile): Promise { 49 | this.constructMessageRenderView({ fileToRender: file }); 50 | this.fileToRender = file; 51 | } 52 | 53 | async constructMessageRenderView(params: { fileToRender: TFile }) { 54 | this.destroy(); 55 | await renderMsgFileToElement({ 56 | msgFile: params.fileToRender, 57 | targetEl: this.contentEl, 58 | plugin: this.plugin, 59 | }); 60 | } 61 | 62 | async onClose(): Promise { 63 | this.plugin.cleanLoadedBlobs({ all: false, forMsgFile: this.fileToRender }); 64 | } 65 | 66 | async onUnloadFile(file: TFile): Promise { 67 | this.contentEl.innerHTML = ''; 68 | super.onUnloadFile(file); 69 | } 70 | } 71 | 72 | /* ------------ SEARCH VIEW FOR MSG CONTENTS ------------ */ 73 | 74 | export const SEARCH_VIEW_DISPLAY_TEXT = 'MSG Handler Search'; 75 | export const SEARCH_VIEW_TYPE = 'msg-handler-search-view'; 76 | export const ICON = 'MSG_HANDLER_ENVELOPE_ICON'; 77 | 78 | export class MsgHandlerSearchView extends ItemView { 79 | plugin: MsgHandlerPlugin; 80 | 81 | constructor(leaf: WorkspaceLeaf, plugin: MsgHandlerPlugin) { 82 | super(leaf); 83 | this.plugin = plugin; 84 | } 85 | 86 | getViewType(): string { 87 | return SEARCH_VIEW_TYPE; 88 | } 89 | 90 | getDisplayText(): string { 91 | return SEARCH_VIEW_DISPLAY_TEXT; 92 | } 93 | 94 | getIcon(): string { 95 | return ICON; 96 | } 97 | 98 | destroy() { 99 | ReactDOM.unmountComponentAtNode(this.contentEl); 100 | } 101 | 102 | async onClose() { 103 | this.destroy(); 104 | } 105 | 106 | async onOpen(): Promise { 107 | this.destroy(); 108 | this.constructMsgSearchView(); 109 | } 110 | 111 | constructMsgSearchView() { 112 | this.destroy(); 113 | ReactDOM.render( 114 |
115 | 116 |
, 117 | this.contentEl 118 | ); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/components/renderer/index.tsx: -------------------------------------------------------------------------------- 1 | import MsgHandlerPlugin from 'main'; 2 | import { TFile } from 'obsidian'; 3 | import React, { useEffect, useState } from 'react'; 4 | import { MSGAttachment, MSGRecipient, MSGRenderData } from 'types'; 5 | import { getMsgContent } from 'utils'; 6 | import { MdKeyboardArrowDown, MdKeyboardArrowRight, MdClose } from 'react-icons/md'; 7 | import { HiChevronDoubleRight, HiChevronDoubleLeft } from 'react-icons/hi'; 8 | import { FolderToSaveSuggestionModal } from 'modals'; 9 | 10 | /* ------------ Main Renderer Component ------------ */ 11 | 12 | export default function RendererViewComponent(params: { plugin: MsgHandlerPlugin; fileToRender: TFile }) { 13 | const { plugin, fileToRender } = params; 14 | const [messageContent, setMessageContent] = useState(); 15 | 16 | useEffect(() => { 17 | getMsgContent({ plugin: plugin, msgFile: fileToRender }).then((msgContent) => { 18 | setMessageContent(msgContent); 19 | }); 20 | }, []); 21 | 22 | return ( 23 | messageContent && ( 24 | <> 25 | 26 | 27 | {messageContent.attachments.length > 0 && ( 28 | 29 | )} 30 | 31 | ) 32 | ); 33 | } 34 | 35 | /* ------------ Child Components ------------ */ 36 | 37 | const MSGHeaderComponent = (params: { messageContent: MSGRenderData }) => { 38 | const { messageContent } = params; 39 | const [open, setOpen] = useState(true); 40 | const toggleOpen = () => setOpen(!open); 41 | return ( 42 | <> 43 |

44 | 45 | Message Header 46 |

47 | {open && ( 48 |
49 | From: {messageContent.senderName} 50 | {' <'} 51 | 57 | {messageContent.senderEmail} 58 | 59 | {'>'} 60 |

61 | Recipients:

62 | Sent: {messageContent.creationTime}

63 | Subject: {messageContent.subject} 64 |
65 | )} 66 | 67 | ); 68 | }; 69 | 70 | const MSGBodyComponent = (params: { messageContent: MSGRenderData }) => { 71 | const { messageContent } = params; 72 | const cleanMsgBody = (txt: string) => txt.replace(/[\r\n]+/g, '
'); 73 | const [open, setOpen] = useState(true); 74 | const toggleOpen = () => setOpen(!open); 75 | return ( 76 | <> 77 |

78 | 79 | Message Body 80 |

81 | {open && ( 82 |
85 | )} 86 | 87 | ); 88 | }; 89 | 90 | const MSGAttachmentsComponent = (params: { messageAttachments: MSGAttachment[]; plugin: MsgHandlerPlugin }) => { 91 | const { messageAttachments, plugin } = params; 92 | const [open, setOpen] = useState(true); 93 | const toggleOpen = () => setOpen(!open); 94 | return ( 95 | <> 96 |

97 | 98 | Attachments 99 |

100 | {open && ( 101 |
102 | {messageAttachments.map((attachment) => { 103 | return ( 104 | 109 | ); 110 | })} 111 |
112 | )} 113 | 114 | ); 115 | }; 116 | 117 | const MSGSingleAttachmentComponent = (params: { messageAttachment: MSGAttachment; plugin: MsgHandlerPlugin }) => { 118 | const { messageAttachment, plugin } = params; 119 | const [open, setOpen] = useState(false); 120 | const toggleOpen = () => setOpen(!open); 121 | 122 | const saveFileToVault = () => { 123 | let modal = new FolderToSaveSuggestionModal( 124 | plugin.app, 125 | messageAttachment.fileBase64, 126 | messageAttachment.fileName 127 | ); 128 | modal.open(); 129 | }; 130 | 131 | const imgExtensions: string[] = ['.png', 'png', '.jpg', 'jpg', '.jpeg', 'jpeg']; 132 | 133 | return ( 134 |
135 |
136 | {imgExtensions.includes(messageAttachment.fileExtension) ? ( 137 | 138 | ) : ( 139 | 140 | )} 141 | {messageAttachment.fileName} 142 | 143 |
144 | {open && ( 145 |
146 | {imgExtensions.includes(messageAttachment.fileExtension) && ( 147 | 148 | )} 149 |
150 | )} 151 |
152 | ); 153 | }; 154 | 155 | /* ------------ Helper Components ------------ */ 156 | 157 | const RecipientList = (params: { recipients: MSGRecipient[] }) => { 158 | const { recipients } = params; 159 | const [open, setOpen] = useState(); 160 | 161 | const moreThanOneRecipient = recipients.length > 1; 162 | 163 | useEffect(() => setOpen(!moreThanOneRecipient), []); 164 | 165 | return ( 166 | <> 167 | {moreThanOneRecipient && 168 | (open ? ( 169 | setOpen(false)} 172 | size="18" 173 | /> 174 | ) : ( 175 | setOpen(true)} 178 | size="18" 179 | /> 180 | ))} 181 | {open && 182 | recipients.map((recipient) => { 183 | return ( 184 | 185 | {recipient.name} 186 | {' <'} 187 | 193 | {recipient.email} 194 | 195 | {'>'} 196 | {recipients.length > 1 ? '; ' : ''} 197 | 198 | ); 199 | })} 200 | 201 | ); 202 | }; 203 | 204 | const ToggleIndicator = (params: { open: boolean }) => { 205 | const { open } = params; 206 | return open ? ( 207 | 208 | ) : ( 209 | 210 | ); 211 | }; 212 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { Plugin, TFile, WorkspaceLeaf, addIcon } from 'obsidian'; 2 | import { 3 | RENDER_VIEW_TYPE, 4 | MsgHandlerView, 5 | MsgHandlerSearchView, 6 | SEARCH_VIEW_TYPE, 7 | ICON, 8 | renderMsgFileToElement, 9 | } from 'view'; 10 | import { getMsgContent } from 'utils'; 11 | import { MSG_HANDLER_ENVELOPE_ICON } from 'icons'; 12 | import { MSGHandlerPluginSettings, MSGHandlerPluginSettingsTab, DEFAULT_SETTINGS } from 'settings'; 13 | import { 14 | createDBMessageContent, 15 | deleteDBMessageContentById, 16 | getDBMessageContentsByPath, 17 | syncDatabaseWithVaultFiles, 18 | updateFilePathOfAllRecords, 19 | } from 'database'; 20 | 21 | export default class MsgHandlerPlugin extends Plugin { 22 | acceptedExtensions: string[] = ['msg', 'eml']; 23 | settings: MSGHandlerPluginSettings; 24 | ribbonIconEl: HTMLElement | undefined = undefined; 25 | 26 | async onload() { 27 | // --> Add Icons 28 | addIcon(ICON, MSG_HANDLER_ENVELOPE_ICON); 29 | 30 | // --> Load Settings 31 | this.addSettingTab(new MSGHandlerPluginSettingsTab(this.app, this)); 32 | await this.loadSettings(); 33 | 34 | // --> Register Plugin Render View 35 | this.registerView(RENDER_VIEW_TYPE, (leaf: WorkspaceLeaf) => { 36 | return new MsgHandlerView(leaf, this); 37 | }); 38 | 39 | // --> Register Plugin Search View 40 | this.registerView(SEARCH_VIEW_TYPE, (leaf) => { 41 | return new MsgHandlerSearchView(leaf, this); 42 | }); 43 | 44 | // --> Register Extension for 'msg' file rendering 45 | this.registerMsgExtensionView(); 46 | 47 | // --> During initial load sync vault msg files with DB and open Search 48 | this.app.workspace.onLayoutReady(() => { 49 | syncDatabaseWithVaultFiles({ plugin: this }).then(() => { 50 | if (this.settings.logEnabled) console.log('Vault DB Sync is completed for MSG Files'); 51 | }); 52 | this.openMsgHandlerSearchLeaf({ showAfterAttach: false }); 53 | }); 54 | 55 | // --> Preview Render 56 | this.registerMarkdownPostProcessor((el, ctx) => { 57 | let msgElement = 58 | el.querySelector('.internal-embed[src$=".eml"]') || 59 | el.querySelector('.internal-embed[src$=".msg"]'); 60 | if (msgElement) { 61 | let src = msgElement.getAttribute('src'); 62 | if (src) { 63 | let msgFile = this.app.metadataCache.getFirstLinkpathDest(src, ctx.sourcePath); 64 | if (msgFile) { 65 | // Remove the default msg render from preview 66 | let parentMsgElement = msgElement.parentElement; 67 | msgElement.remove(); 68 | // Create new div to render msg 69 | let wrapperDiv = parentMsgElement.createEl('div'); 70 | wrapperDiv.addClass('oz-msg-handler-preview-render'); 71 | // Render to the new div 72 | this.renderMSG({ 73 | msgFile: msgFile as TFile, 74 | targetEl: wrapperDiv, 75 | }); 76 | } 77 | } 78 | } 79 | }); 80 | 81 | // --> Add Commands 82 | this.addCommand({ 83 | id: 'reveal-msg-handler-search-leaf', 84 | name: 'Reveal Search Leaf', 85 | callback: () => { 86 | this.openMsgHandlerSearchLeaf({ showAfterAttach: true }); 87 | }, 88 | }); 89 | 90 | // --> Add Event listeners for vault file changes (create, delete, rename) 91 | this.app.vault.on('create', this.handleFileCreate); 92 | this.app.vault.on('delete', this.handleFileDelete); 93 | this.app.vault.on('rename', this.handleFileRename); 94 | 95 | // Ribbon Icon For Opening 96 | this.ribbonIconEl = this.addRibbonIcon(ICON, 'MSG Handler', async () => { 97 | await this.openMsgHandlerSearchLeaf({ showAfterAttach: true }); 98 | }); 99 | } 100 | 101 | onunload() { 102 | // --> Delete event listeners onunload 103 | this.app.vault.off('create', this.handleFileCreate); 104 | this.app.vault.off('delete', this.handleFileDelete); 105 | this.app.vault.off('rename', this.handleFileRename); 106 | } 107 | 108 | // @API - SHARED WITH OZAN'S IMAGE IN EDITOR - DO NOT CHANGE OR SYNC BEFORE 109 | renderMSG = async (params: { msgFile: TFile; targetEl: HTMLElement }) => { 110 | const { msgFile, targetEl } = params; 111 | await renderMsgFileToElement({ 112 | msgFile: msgFile, 113 | targetEl: targetEl, 114 | plugin: this, 115 | }); 116 | }; 117 | 118 | // @API - SHARED WITH OZAN'S IMAGE IN EDITOR - DO NOT CHANGE OR SYNC BEFORE 119 | cleanLoadedBlobs = (params: { all: boolean; forMsgFile?: TFile }) => { 120 | /* Deprecated - Blobs are not created anymore */ 121 | }; 122 | 123 | openMsgHandlerSearchLeaf = async (params: { showAfterAttach: boolean }) => { 124 | const { showAfterAttach } = params; 125 | let leafs = this.app.workspace.getLeavesOfType(SEARCH_VIEW_TYPE); 126 | if (leafs.length === 0) { 127 | let leaf = this.app.workspace.getLeftLeaf(false); 128 | await leaf.setViewState({ type: SEARCH_VIEW_TYPE }); 129 | if (showAfterAttach) this.app.workspace.revealLeaf(leaf); 130 | } else { 131 | if (showAfterAttach && leafs.length > 0) { 132 | this.app.workspace.revealLeaf(leafs[0]); 133 | } 134 | } 135 | }; 136 | 137 | detachMsgHandlerSearchLeaf = () => { 138 | let leafs = this.app.workspace.getLeavesOfType(SEARCH_VIEW_TYPE); 139 | for (let leaf of leafs) { 140 | (leaf.view as MsgHandlerSearchView).destroy(); 141 | leaf.detach(); 142 | } 143 | }; 144 | 145 | registerMsgExtensionView = () => { 146 | try { 147 | this.registerExtensions(this.acceptedExtensions, RENDER_VIEW_TYPE); 148 | } catch (err) { 149 | if (this.settings.logEnabled) console.log('Msg file extension renderer was already registered'); 150 | } 151 | }; 152 | 153 | async loadSettings() { 154 | this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); 155 | } 156 | 157 | async saveSettings() { 158 | await this.saveData(this.settings); 159 | } 160 | 161 | /* --------------- EVENT HANDLERS FOR VAULT FILE CHANGES -------------- */ 162 | 163 | /** 164 | * This function is created to handle "create" event for vault 165 | * @param file 166 | */ 167 | handleFileCreate = async (file: TFile) => { 168 | if (this.acceptedExtensions.contains(file.extension)) { 169 | let dbMsgContents = await getDBMessageContentsByPath({ filePath: file.path }); 170 | if (dbMsgContents.length === 0) { 171 | let msgContent = await getMsgContent({ plugin: this, msgFile: file }); 172 | createDBMessageContent({ 173 | msgContent: msgContent, 174 | file: file as TFile, 175 | }); 176 | if (this.settings.logEnabled) console.log(`DB Index Record is created for ${file.path}`); 177 | } 178 | } 179 | }; 180 | 181 | /** 182 | * This function is created to handle "delete" event for vault 183 | * @param file 184 | */ 185 | handleFileDelete = async (file: TFile) => { 186 | if (this.acceptedExtensions.contains(file.extension)) { 187 | let dbMsgContents = await getDBMessageContentsByPath({ filePath: file.path }); 188 | if (dbMsgContents.length > 0) { 189 | for (let dbMsgContent of dbMsgContents) { 190 | await deleteDBMessageContentById({ id: dbMsgContent.id }); 191 | if (this.settings.logEnabled) console.log(`DB Index Record is deleted for ${file.path}`); 192 | } 193 | } 194 | } 195 | }; 196 | 197 | /** 198 | * This function is created to handle "rename" event for vault 199 | * @param file 200 | * @param oldPath 201 | */ 202 | handleFileRename = async (file: TFile, oldPath: string) => { 203 | await updateFilePathOfAllRecords({ oldValue: oldPath, newValue: file.path }); 204 | if (this.settings.logEnabled) console.log(`DB Index Record is updated for ${file.path}`); 205 | }; 206 | } 207 | -------------------------------------------------------------------------------- /src/database.ts: -------------------------------------------------------------------------------- 1 | import Dexie from 'dexie'; 2 | import { TFile } from 'obsidian'; 3 | import MsgHandlerPlugin from 'main'; 4 | import { MSGDataIndexed, MSGBaseData, MSGDataIndexedSearchEligible } from 'types'; 5 | import { getMsgContent } from 'utils'; 6 | import fuzzysort from 'fuzzysort'; 7 | 8 | // --> Custom Class from Dexie to Handle Indexed DB 9 | export class MsgHandlerDatabase extends Dexie { 10 | dbMessageContents!: Dexie.Table; 11 | 12 | constructor() { 13 | super('MsgHandlerDatabase'); 14 | this.version(1).stores({ 15 | dbMessageContents: '++id, senderName, senderEmail, recipients, subject, body, &filePath, mtime', 16 | }); 17 | } 18 | } 19 | 20 | // --> Create Custom Class DB Instance 21 | const pluginDb = new MsgHandlerDatabase(); 22 | 23 | /** 24 | * Update the records with old file path record with new file path record 25 | * @param params 26 | */ 27 | export const updateFilePathOfAllRecords = async (params: { oldValue: string; newValue: string }) => { 28 | const { oldValue, newValue } = params; 29 | await pluginDb.dbMessageContents.where('filePath').equals(oldValue).modify({ 30 | filePath: newValue, 31 | }); 32 | }; 33 | 34 | /** 35 | * Get all saved/synced message contents from Database 36 | * @returns Promise 37 | */ 38 | export const getAllDBMessageContents = async (): Promise => { 39 | return await pluginDb.dbMessageContents.toArray(); 40 | }; 41 | 42 | /** 43 | * Get only message content with provided filePath from the database 44 | * @param { filePath: string } 45 | * @returns Promise 46 | */ 47 | export const getDBMessageContentsByPath = async (params: { filePath: string }): Promise => { 48 | const { filePath } = params; 49 | return await pluginDb.dbMessageContents.where('filePath').equals(filePath).toArray(); 50 | }; 51 | 52 | /** 53 | * This function will save provided msgContent with the meta data coming from file into the database 54 | * @param { msgContent: CustomMessageContent, file: TFile } 55 | */ 56 | export const createDBMessageContent = async (params: { msgContent: MSGBaseData; file: TFile }) => { 57 | const { msgContent, file } = params; 58 | await pluginDb.dbMessageContents.add({ 59 | senderName: msgContent.senderName, 60 | senderEmail: msgContent.senderEmail, 61 | recipients: msgContent.recipients, 62 | creationTime: msgContent.creationTime, 63 | body: msgContent.body, 64 | subject: msgContent.subject, 65 | filePath: file.path, 66 | mtime: file.stat.mtime, 67 | } as MSGDataIndexed); 68 | }; 69 | 70 | /** 71 | * Delete Message Content By Id from the Database with the provided id 72 | * @param { id: number | undefined } 73 | */ 74 | export const deleteDBMessageContentById = async (params: { id: number | undefined }) => { 75 | if (params.id) { 76 | await pluginDb.dbMessageContents.delete(params.id); 77 | } 78 | }; 79 | 80 | /** 81 | * This function is designed to cross check vault msg files with db message contents and sync them 82 | * @param { plugin: MsgHandlerPlugin } 83 | */ 84 | export const syncDatabaseWithVaultFiles = async (params: { plugin: MsgHandlerPlugin }) => { 85 | const { plugin } = params; 86 | 87 | let msgFiles = plugin.app.vault.getFiles().filter((f) => plugin.acceptedExtensions.contains(f.extension)); 88 | let dbMsgContents = await getAllDBMessageContents(); 89 | 90 | // Loop db message contents to see if they exist in the vault 91 | for (let dbMsgContent of dbMsgContents) { 92 | if (!msgFiles.some((f) => f.path == dbMsgContent.filePath)) { 93 | await deleteDBMessageContentById({ id: dbMsgContent.id }); 94 | } 95 | } 96 | // Create the msgFiles in DB, which do not exist 97 | for (let msgFile of msgFiles) { 98 | if (!dbMsgContents.some((c) => c.filePath === msgFile.path)) { 99 | let msgContent = await getMsgContent({ plugin: plugin, msgFile: msgFile }); 100 | await createDBMessageContent({ 101 | msgContent: msgContent, 102 | file: msgFile, 103 | }); 104 | } 105 | } 106 | }; 107 | 108 | /** 109 | * This will search Indexed DB with the provided key and return Fuzzy Results 110 | * @param params 111 | * @returns 112 | */ 113 | export const searchMsgFilesWithKey = async (params: { key: string }) => { 114 | // Get all Message Contents from DB Indexed 115 | let allDBMessageContents: MSGDataIndexed[] = await getAllDBMessageContents(); 116 | // Create New Variable to Store the Contents and Convert all Fields to String 117 | let searchConvenientMessageContents: MSGDataIndexedSearchEligible[] = allDBMessageContents.map( 118 | (messageContent) => ({ 119 | ...messageContent, 120 | recipients: messageContent.recipients.map((r) => r.name + ' <' + r.email + '>').join(', '), 121 | }) 122 | ); 123 | // Evaluate the fields and get the best result 124 | const results: Fuzzysort.KeysResults = fuzzysort.go( 125 | params.key, 126 | searchConvenientMessageContents, 127 | { 128 | keys: ['senderName', 'senderEmail', 'subject', 'body', 'recipients'], 129 | threshold: -20000, 130 | scoreFn: (a) => { 131 | const searchKey = params.key.toLowerCase(); 132 | const exactMatch = 133 | a[0]?.target.toLowerCase().includes(searchKey) || 134 | a[1]?.target.toLowerCase().includes(searchKey) || 135 | a[2]?.target.toLowerCase().includes(searchKey) || 136 | a[3]?.target.toLowerCase().includes(searchKey) || 137 | a[4]?.target.toLowerCase().includes(searchKey); 138 | if (exactMatch) { 139 | return 0; 140 | } else { 141 | // Use the original fuzzysort score for all other matches 142 | let senderNameScore = a[0] ? a[0].score : -100000; 143 | let senderEmailScore = a[1] ? a[1].score : -100000; 144 | let subjectScore = a[2] ? a[2].score : -100000; 145 | let bodyScore = a[3] ? a[3].score : -100000; 146 | let recipientsScore = a[4] ? a[4].score : -100000; 147 | return Math.max(senderNameScore, senderEmailScore, subjectScore, bodyScore, recipientsScore); 148 | } 149 | }, 150 | } 151 | ); 152 | return results; 153 | }; 154 | 155 | /** 156 | * Pass the string coming from fuzzysort, which includes items to hightlight 157 | * matches within the text. It will return appropriate part of the text to show 158 | * within the Search results to occupy less place and show relevant part 159 | * @param txt 160 | * @returns 161 | */ 162 | export const getHighlightedPartOfSearchResult = (params: { highlightedResult: string; searchKey: string }) => { 163 | const { highlightedResult, searchKey } = params; 164 | 165 | let maxSearchDisplayLength = 120; 166 | 167 | if (highlightedResult.length < maxSearchDisplayLength) { 168 | return highlightedResult; 169 | } else { 170 | const firstMarkIndex = highlightedResult.indexOf(''); 171 | const lastMarkIndex = highlightedResult.lastIndexOf(''); 172 | 173 | // Return original text if not found 174 | if (firstMarkIndex === -1 || lastMarkIndex === -1) return highlightedResult; 175 | 176 | const searchKeyLength = searchKey.length; 177 | const lengthAfterHighlight = highlightedResult.length - (lastMarkIndex + 7); 178 | const leftUsageLength = maxSearchDisplayLength - searchKeyLength; 179 | const eachSideUsageLength = Math.floor(leftUsageLength / 2); 180 | 181 | let startIndex = 0; 182 | let startMissing = 0; // couldn't get that many characters, add to end if possible 183 | if (firstMarkIndex > eachSideUsageLength) { 184 | // There is more than enough text, extract only limited text 185 | startIndex = firstMarkIndex - eachSideUsageLength; 186 | } else { 187 | // There wasn't enough text enough, try to attach to end 188 | startMissing = eachSideUsageLength - firstMarkIndex; 189 | } 190 | 191 | let endIndex = highlightedResult.length - 1; 192 | if (lengthAfterHighlight > eachSideUsageLength) { 193 | // There is more than enough text, extract only limited text 194 | endIndex = lastMarkIndex + 7 + eachSideUsageLength; 195 | // Try to add more if startMissing is more than 0 196 | let endLeftPlace = highlightedResult.length - 1 - endIndex; 197 | if (endLeftPlace > startMissing) { 198 | endIndex += startMissing; 199 | } else { 200 | endIndex = highlightedResult.length - 1; 201 | } 202 | } else { 203 | // There wasn't enough text at the end, try to attach to the start 204 | let endMissing = eachSideUsageLength - lengthAfterHighlight; 205 | if (endMissing > 0) { 206 | if (startIndex > endMissing) { 207 | startIndex = startIndex - endMissing; 208 | } else { 209 | startIndex = 0; 210 | } 211 | } 212 | } 213 | 214 | return '...' + highlightedResult.substring(startIndex, endIndex) + '...'; 215 | } 216 | }; 217 | -------------------------------------------------------------------------------- /src/components/search/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from 'react'; 2 | import fuzzysort from 'fuzzysort'; 3 | import MsgHandlerPlugin from 'main'; 4 | import { MdKeyboardArrowDown, MdKeyboardArrowRight } from 'react-icons/md'; 5 | import { CgChevronDoubleUp, CgChevronDoubleDown } from 'react-icons/cg'; 6 | import { MSGDataIndexedSearchEligible } from 'types'; 7 | import { searchMsgFilesWithKey, getHighlightedPartOfSearchResult } from 'database'; 8 | import { getFileName, replaceNewLinesAndCarriages, openFile, openFileInNewTab, isMouseEvent } from 'utils'; 9 | import { TFile, Menu } from 'obsidian'; 10 | 11 | type SearchResultSingleItem = { 12 | result: Fuzzysort.KeysResult; 13 | highlightedResult: string; 14 | }; 15 | type SearchResultState = SearchResultSingleItem[]; 16 | type AllOpenStatus = 'open' | 'closed' | null; 17 | 18 | /* ------------ SEARCH FULL VIEW RENDER ------------ */ 19 | 20 | export default function SearchViewComponent(params: { plugin: MsgHandlerPlugin }) { 21 | const { plugin } = params; 22 | 23 | const [searchKey, setSearchKey] = useState(); 24 | const [searchResults, setSearchResults] = useState(); 25 | 26 | // Helper state to collapse/expand all (inherited in the child components) 27 | const [allOpenStatus, setAllOpenStatus] = useState(); 28 | 29 | // Handle Search Promises in Order 30 | const promiseQueueRef = useRef([]); 31 | 32 | useEffect(() => { 33 | const runPromiseQueue = async () => { 34 | while (promiseQueueRef.current.length > 0) { 35 | const nextPromise = promiseQueueRef.current[0]; 36 | try { 37 | await nextPromise(); 38 | } catch (error) { 39 | if (plugin.settings.logEnabled) console.log('Search promise failed', error); 40 | } finally { 41 | promiseQueueRef.current.shift(); // remove the completed promise from the queue 42 | } 43 | } 44 | }; 45 | 46 | if (searchKey === '' || searchKey === null || searchKey === undefined) { 47 | setSearchResults(null); 48 | } else { 49 | promiseQueueRef.current.push(runSearch); 50 | runPromiseQueue(); 51 | } 52 | }, [searchKey]); 53 | 54 | // Cleanup the results if searchkey is empty 55 | const handleInputChange = (event: React.ChangeEvent) => { 56 | const newSearchKey = event.target.value; 57 | setSearchKey(newSearchKey); 58 | }; 59 | 60 | // --> Search Function using Current searchKey 61 | const runSearch = async () => { 62 | let currentSearchResults = []; 63 | // Get search results 64 | let results = await searchMsgFilesWithKey({ key: searchKey }); 65 | // Loop results to populate component state 66 | for (let result of results) { 67 | let indexOfMaxScore = null; 68 | let exactMatch = false; 69 | // First check exact match 70 | const exactMatchIndex = result.findIndex((r) => 71 | r?.target.toLowerCase().includes(searchKey.toLowerCase()) 72 | ); 73 | if (exactMatchIndex !== -1) { 74 | indexOfMaxScore = exactMatchIndex; 75 | exactMatch = true; 76 | } 77 | // If no exact match, Get the best score from fields 78 | else { 79 | const scores = result.map((r) => r?.score ?? -100000); 80 | indexOfMaxScore = scores.reduce( 81 | (maxIndex, score, index) => (score > scores[maxIndex] ? index : maxIndex), 82 | 0 83 | ); 84 | } 85 | // Get highligted html 86 | let highlightedResult = null; 87 | if (exactMatch) { 88 | // Prepare the exact match text manually 89 | let indexOfSearchMatch = result[indexOfMaxScore].target 90 | .toLowerCase() 91 | .indexOf(searchKey.toLowerCase()); 92 | let lengthOfSearchKey = searchKey.length; 93 | let originalTextOfSearchKey = result[indexOfMaxScore].target.substring( 94 | indexOfSearchMatch, 95 | indexOfSearchMatch + lengthOfSearchKey 96 | ); 97 | highlightedResult = result[indexOfMaxScore].target.replace( 98 | originalTextOfSearchKey, 99 | '' + originalTextOfSearchKey + '' 100 | ); 101 | } else { 102 | highlightedResult = fuzzysort.highlight( 103 | result[indexOfMaxScore], 104 | '', 105 | '' 106 | ); 107 | } 108 | 109 | // If there is a highlighted result, cleanup the new line signs 110 | if (highlightedResult) { 111 | highlightedResult = getHighlightedPartOfSearchResult({ 112 | highlightedResult: replaceNewLinesAndCarriages(highlightedResult), 113 | searchKey: searchKey, 114 | }); 115 | } 116 | 117 | // Push for display results 118 | currentSearchResults.push({ 119 | result: result, 120 | highlightedResult: highlightedResult, 121 | }); 122 | } 123 | 124 | // After obtaining all results, push into the component state 125 | setSearchResults(currentSearchResults); 126 | }; 127 | 128 | return ( 129 |
130 |
131 | setAllOpenStatus('closed')} 135 | size={20} 136 | /> 137 | setAllOpenStatus('open')} 141 | size={20} 142 | /> 143 |
144 |
145 | 151 |
152 |
153 | {searchResults && 154 | (searchResults.length > 0 ? ( 155 | searchResults.map((searchResult) => { 156 | return ( 157 | 162 | ); 163 | }) 164 | ) : ( 165 |
No matches found
166 | ))} 167 |
168 |
169 | ); 170 | } 171 | 172 | /* ------------ SINGLE FILE MATCH RESULT VIEW ------------ */ 173 | 174 | const SearchResultFileMatch = (params: { 175 | plugin: MsgHandlerPlugin; 176 | searchResult: SearchResultSingleItem; 177 | allOpenStatus: AllOpenStatus; 178 | }) => { 179 | const { searchResult, allOpenStatus, plugin } = params; 180 | const [open, setOpen] = useState(true); 181 | 182 | useEffect(() => { 183 | if (allOpenStatus === 'open') { 184 | setOpen(true); 185 | } else if (allOpenStatus === 'closed') { 186 | setOpen(false); 187 | } 188 | }, [allOpenStatus]); 189 | 190 | const getCurrentAbstractFile = () => { 191 | return plugin.app.vault.getAbstractFileByPath(searchResult.result.obj.filePath); 192 | }; 193 | 194 | const openFileClicked = (e: React.MouseEvent) => { 195 | let file = getCurrentAbstractFile(); 196 | if (file) { 197 | openFile({ 198 | file: file as TFile, 199 | plugin: plugin, 200 | newLeaf: (e.ctrlKey || e.metaKey) && !(e.shiftKey || e.altKey), 201 | leafBySplit: (e.ctrlKey || e.metaKey) && (e.shiftKey || e.altKey), 202 | }); 203 | } 204 | }; 205 | 206 | // --> AuxClick (Mouse Wheel Button Action) 207 | const onAuxClick = (e: React.MouseEvent) => { 208 | let file = getCurrentAbstractFile(); 209 | if (e.button === 1 && file) openFileInNewTab({ plugin: plugin, file: file as TFile }); 210 | }; 211 | 212 | // --> Context Menu 213 | const triggerContextMenu = (e: React.MouseEvent) => { 214 | const fileMenu = new Menu(); 215 | const filePath = searchResult.result.obj.filePath; 216 | const file = plugin.app.vault.getAbstractFileByPath(filePath); 217 | if (file) { 218 | plugin.app.workspace.trigger('file-menu', fileMenu, file, 'file-explorer'); 219 | if (isMouseEvent) { 220 | fileMenu.showAtPosition({ x: e.pageX, y: e.pageY }); 221 | } 222 | } 223 | }; 224 | 225 | return ( 226 |
227 |
228 |
229 | {open ? ( 230 | setOpen(false)} /> 231 | ) : ( 232 | setOpen(true)} /> 233 | )} 234 |
235 |
240 | {getFileName(searchResult.result.obj.filePath)} 241 |
242 |
243 | {open && searchResult.highlightedResult?.length > 0 && ( 244 |
245 |
250 |
251 | )} 252 |
253 | ); 254 | }; 255 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { MSGReader } from './modules/msgreader.js'; 2 | import MsgHandlerPlugin from 'main'; 3 | import { MarkdownRenderer, Component, TFile } from 'obsidian'; 4 | import { readEml, ReadedEmlJson } from 'eml-parse-js'; 5 | import dayjs from 'dayjs'; 6 | import { Base64 } from 'js-base64'; 7 | import { 8 | MSGRenderData, 9 | MSGRecipient, 10 | MSGAttachment, 11 | Ext_MSGReader_FileData, 12 | Ext_MSGReader_Attachment, 13 | Ext_MSGReader_AttachmentData, 14 | Ext_MSGReader_Recipient, 15 | } from 'types'; 16 | 17 | /** 18 | * This function is to get the MSGRenderData for provided Outlook MSG file under the path provided 19 | * @param params 20 | * @returns 21 | */ 22 | export const getMsgContent = async (params: { 23 | plugin: MsgHandlerPlugin; 24 | msgFile: TFile; 25 | }): Promise => { 26 | const { plugin, msgFile } = params; 27 | if (msgFile.extension === 'msg') { 28 | let msgFileBuffer = await plugin.app.vault.readBinary(params.msgFile); 29 | let msgReader = new MSGReader(msgFileBuffer); 30 | let fileData = msgReader.getFileData() as Ext_MSGReader_FileData; 31 | let creationTime = getMsgDate({ rawHeaders: fileData.headers }); 32 | return { 33 | senderName: dataOrEmpty(fileData.senderName), 34 | senderEmail: dataOrEmpty(fileData.senderEmail), 35 | recipients: getCustomRecipients(fileData.recipients ? fileData.recipients : []), 36 | creationTime: 37 | typeof creationTime === 'string' 38 | ? creationTime 39 | : dayjs(creationTime).format('ddd, D MMM YYYY HH:mm:ss'), 40 | subject: dataOrEmpty(fileData.subject), 41 | body: dataOrEmpty(fileData.body), 42 | attachments: extractMSGAttachments({ 43 | msgReader: msgReader, 44 | fileDataAttachments: fileData.attachments, 45 | }), 46 | }; 47 | } else if (msgFile.extension === 'eml') { 48 | let readedEmlJson = await readEmlFile({ emlFile: msgFile, plugin: plugin }); 49 | let sender = parseEmlSender({ senderText: readedEmlJson.headers.From }); 50 | return { 51 | senderName: sender.senderName, 52 | senderEmail: sender.senderEmail, 53 | recipients: parseEMLRecipients({ readEmlJson: readedEmlJson }), 54 | creationTime: dayjs(readedEmlJson.date).format('ddd, D MMM YYYY HH:mm:ss'), 55 | subject: dataOrEmpty(readedEmlJson.subject), 56 | body: cleanEMLBody({ text: readedEmlJson.text }), 57 | attachments: extractEMLAttachments({ emlFileReadJson: readedEmlJson }), 58 | }; 59 | } 60 | }; 61 | 62 | /** 63 | * Creates Header Dictionary coming from Msg Headers String 64 | * @param params 65 | * @returns { [key: string]: string } 66 | */ 67 | function parseHeaders(params: { headers: string }): { [key: string]: string } { 68 | const { headers } = params; 69 | var parsedHeaders: { [key: string]: string } = {}; 70 | if (!headers) return parsedHeaders; 71 | var headerRegEx = /(.*)\: (.*)/g; 72 | let m; 73 | while ((m = headerRegEx.exec(headers))) { 74 | // todo: Pay attention! Header can be presented many times (e.g. Received). 75 | // Handle it, if needed! 76 | parsedHeaders[m[1]] = m[2]; 77 | } 78 | return parsedHeaders; 79 | } 80 | 81 | /** 82 | * From raw msg headers string, it will extract the creation time 83 | * @param params 84 | * @returns 85 | */ 86 | function getMsgDate(params: { rawHeaders: string }): string | Date { 87 | const { rawHeaders } = params; 88 | // Example for the Date header 89 | var headers = parseHeaders({ headers: rawHeaders }); 90 | if (!headers['Date']) { 91 | return '-'; 92 | } 93 | return new Date(headers['Date']); 94 | } 95 | 96 | /** 97 | * Function to clean the EML Body Text 98 | * @param params 99 | * @returns 100 | */ 101 | const cleanEMLBody = (params: { text: string }) => { 102 | if (!params.text) return ''; 103 | let cleanTxt = params.text.replace(/\r\n\r\n/g, '\r\n\r\n \r\n\r\n'); 104 | const pattern = /\[cid:.*?\]/g; 105 | return cleanTxt.replace(pattern, ''); 106 | }; 107 | 108 | /** 109 | * Reads EML TFile And Returns ReadedEmlJson Format 110 | * @param params 111 | * @returns 112 | */ 113 | const readEmlFile = async (params: { emlFile: TFile; plugin: MsgHandlerPlugin }): Promise => { 114 | const { emlFile, plugin } = params; 115 | let emlFileRead = await plugin.app.vault.read(emlFile); 116 | return new Promise((resolve, reject) => { 117 | readEml(emlFileRead, (err, ReadedEMLJson) => { 118 | if (err) { 119 | reject(err); 120 | } else { 121 | resolve(ReadedEMLJson); 122 | } 123 | }); 124 | }); 125 | }; 126 | 127 | /** 128 | * Get the "TO" Sender Text and Render Sender Name and Sender Email from it 129 | * @param params 130 | * @returns 131 | */ 132 | const parseEmlSender = (params: { senderText: string }): { senderName: string; senderEmail: string } => { 133 | let { senderText } = params; 134 | if (senderText === '' || senderText === undefined || senderText === null) { 135 | return { senderName: '', senderEmail: '' }; 136 | } 137 | senderText = senderText.replace(/"/g, ''); 138 | const regex = /^([^<]+) <([^>]+)>$/; 139 | const match = regex.exec(senderText); 140 | if (!match) return { senderName: '', senderEmail: '' }; 141 | const [, senderName, senderEmail] = match; 142 | return { senderName, senderEmail }; 143 | }; 144 | 145 | /** 146 | * From EML To and CC create MSGRecipient List 147 | * @param params 148 | * @returns 149 | */ 150 | const parseEMLRecipients = (params: { readEmlJson: ReadedEmlJson }): MSGRecipient[] => { 151 | const { readEmlJson } = params; 152 | let emlTo = dataOrEmpty(readEmlJson.headers.To); 153 | let emlCC = dataOrEmpty(readEmlJson.headers.CC); 154 | let recipientsText = emlTo + (emlCC === '' ? '' : ', ' + emlCC); 155 | let recipientsTextSplit = recipientsText.split('>,'); 156 | const regex = /"([^"]+)"\s*\s]+)>?/; 157 | let msgRecipients = []; 158 | for (let recipientText of recipientsTextSplit) { 159 | const match = recipientText.match(regex); 160 | if (match) { 161 | const name = match[1] || match[3]; 162 | const email = match[2] || match[4]; 163 | msgRecipients.push({ name, email }); 164 | } 165 | } 166 | return msgRecipients; 167 | }; 168 | 169 | /** 170 | * This function is to extract attachments coming from MsgReader Library and convert into MSGAttachment that 171 | * is later consumed by MSGRenderData 172 | * @param params 173 | * @returns 174 | */ 175 | const extractMSGAttachments = (params: { 176 | msgReader: MSGReader; 177 | fileDataAttachments: Ext_MSGReader_Attachment[]; 178 | }): MSGAttachment[] => { 179 | const { msgReader, fileDataAttachments } = params; 180 | let msgAttachments: MSGAttachment[] = []; 181 | for (let [index, fileDataAttachment] of fileDataAttachments.entries()) { 182 | let attRead = msgReader.getAttachment(index) as Ext_MSGReader_AttachmentData; 183 | msgAttachments.push({ 184 | fileName: attRead.fileName, 185 | fileExtension: fileDataAttachment.extension, 186 | fileBase64: attRead.content ? uint8ArrayToBase64(attRead.content) : null, 187 | }); 188 | } 189 | return msgAttachments; 190 | }; 191 | 192 | /** 193 | * Extract Attachments from EML File Read 194 | * @param params 195 | * @returns 196 | */ 197 | const extractEMLAttachments = (params: { emlFileReadJson: ReadedEmlJson }): MSGAttachment[] => { 198 | const { emlFileReadJson } = params; 199 | 200 | if (emlFileReadJson.attachments && emlFileReadJson.attachments.length > 0) { 201 | let attachments: MSGAttachment[] = []; 202 | for (let attachment of params.emlFileReadJson.attachments) { 203 | let fileNameParts = attachment.name.split('.'); 204 | let extension = fileNameParts[fileNameParts.length - 1]; 205 | attachments.push({ 206 | fileName: attachment.name, 207 | fileExtension: '.' + extension, 208 | fileBase64: attachment.data64, 209 | }); 210 | } 211 | return attachments; 212 | } else { 213 | return []; 214 | } 215 | }; 216 | 217 | /** 218 | * This function is to convert Recipients from MsgReader Library format to MSGRecipient format 219 | * @param recipients 220 | * @returns 221 | */ 222 | const getCustomRecipients = (recipients: Ext_MSGReader_Recipient[]): MSGRecipient[] => { 223 | if (recipients && recipients.length > 0) { 224 | let customRecipients = []; 225 | for (let recipient of recipients) { 226 | customRecipients.push({ 227 | name: dataOrEmpty(recipient.name), 228 | email: dataOrEmpty(recipient.email), 229 | }); 230 | } 231 | return customRecipients; 232 | } else { 233 | return []; 234 | } 235 | }; 236 | 237 | /** 238 | * Checks object if it is null and returns empty string if null 239 | * @param data 240 | * @returns 241 | */ 242 | const dataOrEmpty = (data: any) => { 243 | return data ? data : ''; 244 | }; 245 | 246 | /** 247 | * Obsidians native markdown renderer function 248 | * @param mdContent 249 | * @param destEl 250 | */ 251 | export const renderMarkdown = async (mdContent: string, destEl: HTMLElement) => { 252 | await MarkdownRenderer.renderMarkdown(mdContent, destEl, '', null as unknown as Component); 253 | }; 254 | 255 | /** 256 | * Helps to cleanup the new line signs within Rich Text Editor format, which is \r\n 257 | * @param txt 258 | * @returns 259 | */ 260 | export const replaceNewLinesAndCarriages = (txt: string) => { 261 | return txt?.replace(/[\r\n]+/g, ''); 262 | }; 263 | 264 | /** 265 | * This function extracts the file name (including extension) from a full file path 266 | * @param filePath 267 | * @returns 268 | */ 269 | export const getFileName = (filePath: string) => { 270 | var index = filePath.lastIndexOf('/'); 271 | if (index !== -1) return filePath.substring(index + 1); 272 | return filePath; 273 | }; 274 | 275 | /** 276 | * Helper to open a file passed in params within Obsidian (Tab/Separate) 277 | * @param params 278 | */ 279 | export const openFile = (params: { 280 | file: TFile; 281 | plugin: MsgHandlerPlugin; 282 | newLeaf: boolean; 283 | leafBySplit?: boolean; 284 | }) => { 285 | const { file, plugin, newLeaf, leafBySplit } = params; 286 | let leaf = plugin.app.workspace.getLeaf(newLeaf); 287 | if (!newLeaf && leafBySplit) leaf = plugin.app.workspace.createLeafBySplit(leaf, 'vertical'); 288 | plugin.app.workspace.setActiveLeaf(leaf, { focus: true }); 289 | leaf.openFile(file, { eState: { focus: true } }); 290 | }; 291 | 292 | /** 293 | * This function will open the file provided as a new tab 294 | * @param params 295 | */ 296 | export const openFileInNewTab = (params: { plugin: MsgHandlerPlugin; file: TFile }) => { 297 | openFile({ file: params.file, plugin: params.plugin, newLeaf: true }); 298 | }; 299 | 300 | /** 301 | * This function will open the file as a separate column and will create a new split leaf 302 | * @param params 303 | */ 304 | export const openFileInNewTabGroup = (params: { plugin: MsgHandlerPlugin; file: TFile }) => { 305 | openFile({ file: params.file, plugin: params.plugin, newLeaf: false, leafBySplit: true }); 306 | }; 307 | 308 | /** 309 | * Check if event is a mouse event 310 | * @param e 311 | * @returns 312 | */ 313 | export function isMouseEvent(e: React.TouchEvent | React.MouseEvent): e is React.MouseEvent { 314 | return e && 'screenX' in e; 315 | } 316 | 317 | /** 318 | * Convert base64 string to Uint8Array 319 | * @param base64 320 | * @returns 321 | */ 322 | export function base64ToArrayBuffer(base64: string): Uint8Array { 323 | var binary_string = window.atob(base64); 324 | var len = binary_string.length; 325 | var bytes = new Uint8Array(len); 326 | for (var i = 0; i < len; i++) { 327 | bytes[i] = binary_string.charCodeAt(i); 328 | } 329 | return bytes; 330 | } 331 | 332 | /** 333 | * Converts uint8Array to Base64 String 334 | * @param uint8Array 335 | * @returns 336 | */ 337 | export function uint8ArrayToBase64(uint8Array: Uint8Array): string { 338 | return Base64.fromUint8Array(uint8Array); 339 | } 340 | -------------------------------------------------------------------------------- /src/modules/msgreader.js: -------------------------------------------------------------------------------- 1 | /* Copyright 2021 Yury Karpovich 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | /** 17 | * This MSG Reader is created by Yury Karpovich 18 | * Under Github Repository https://github.com/ykarpovich/msg.reader 19 | */ 20 | 21 | /** 22 | DataStream reads scalars, arrays and structs of data from an ArrayBuffer. 23 | It's like a file-like DataView on steroids. 24 | 25 | @param {ArrayBuffer} arrayBuffer ArrayBuffer to read from. 26 | @param {?Number} byteOffset Offset from arrayBuffer beginning for the DataStream. 27 | @param {?Boolean} endianness DataStream.BIG_ENDIAN or DataStream.LITTLE_ENDIAN (the default). 28 | */ 29 | DataStream = function (arrayBuffer, byteOffset, endianness) { 30 | this._byteOffset = byteOffset || 0; 31 | if (arrayBuffer instanceof ArrayBuffer) { 32 | this.buffer = arrayBuffer; 33 | } else if (typeof arrayBuffer == 'object') { 34 | this.dataView = arrayBuffer; 35 | if (byteOffset) { 36 | this._byteOffset += byteOffset; 37 | } 38 | } else { 39 | this.buffer = new ArrayBuffer(arrayBuffer || 1); 40 | } 41 | this.position = 0; 42 | this.endianness = endianness == null ? DataStream.LITTLE_ENDIAN : endianness; 43 | }; 44 | DataStream.prototype = {}; 45 | 46 | /* Fix for Opera 12 not defining BYTES_PER_ELEMENT in typed array prototypes. */ 47 | if (Uint8Array.prototype.BYTES_PER_ELEMENT === undefined) { 48 | Uint8Array.prototype.BYTES_PER_ELEMENT = Uint8Array.BYTES_PER_ELEMENT; 49 | Int8Array.prototype.BYTES_PER_ELEMENT = Int8Array.BYTES_PER_ELEMENT; 50 | Uint8ClampedArray.prototype.BYTES_PER_ELEMENT = Uint8ClampedArray.BYTES_PER_ELEMENT; 51 | Uint16Array.prototype.BYTES_PER_ELEMENT = Uint16Array.BYTES_PER_ELEMENT; 52 | Int16Array.prototype.BYTES_PER_ELEMENT = Int16Array.BYTES_PER_ELEMENT; 53 | Uint32Array.prototype.BYTES_PER_ELEMENT = Uint32Array.BYTES_PER_ELEMENT; 54 | Int32Array.prototype.BYTES_PER_ELEMENT = Int32Array.BYTES_PER_ELEMENT; 55 | Float64Array.prototype.BYTES_PER_ELEMENT = Float64Array.BYTES_PER_ELEMENT; 56 | } 57 | 58 | /** 59 | Saves the DataStream contents to the given filename. 60 | Uses Chrome's anchor download property to initiate download. 61 | 62 | @param {string} filename Filename to save as. 63 | @return {null} 64 | */ 65 | DataStream.prototype.save = function (filename) { 66 | var blob = new Blob(this.buffer); 67 | var URL = window.webkitURL || window.URL; 68 | if (URL && URL.createObjectURL) { 69 | var url = URL.createObjectURL(blob); 70 | var a = document.createElement('a'); 71 | a.setAttribute('href', url); 72 | a.setAttribute('download', filename); 73 | a.click(); 74 | URL.revokeObjectURL(url); 75 | } else { 76 | throw "DataStream.save: Can't create object URL."; 77 | } 78 | }; 79 | 80 | /** 81 | Big-endian const to use as default endianness. 82 | @type {boolean} 83 | */ 84 | DataStream.BIG_ENDIAN = false; 85 | 86 | /** 87 | Little-endian const to use as default endianness. 88 | @type {boolean} 89 | */ 90 | DataStream.LITTLE_ENDIAN = true; 91 | 92 | /** 93 | Whether to extend DataStream buffer when trying to write beyond its size. 94 | If set, the buffer is reallocated to twice its current size until the 95 | requested write fits the buffer. 96 | @type {boolean} 97 | */ 98 | DataStream.prototype._dynamicSize = true; 99 | Object.defineProperty(DataStream.prototype, 'dynamicSize', { 100 | get: function () { 101 | return this._dynamicSize; 102 | }, 103 | set: function (v) { 104 | if (!v) { 105 | this._trimAlloc(); 106 | } 107 | this._dynamicSize = v; 108 | }, 109 | }); 110 | 111 | /** 112 | Virtual byte length of the DataStream backing buffer. 113 | Updated to be max of original buffer size and last written size. 114 | If dynamicSize is false is set to buffer size. 115 | @type {number} 116 | */ 117 | DataStream.prototype._byteLength = 0; 118 | 119 | /** 120 | Returns the byte length of the DataStream object. 121 | @type {number} 122 | */ 123 | Object.defineProperty(DataStream.prototype, 'byteLength', { 124 | get: function () { 125 | return this._byteLength - this._byteOffset; 126 | }, 127 | }); 128 | 129 | /** 130 | Set/get the backing ArrayBuffer of the DataStream object. 131 | The setter updates the DataView to point to the new buffer. 132 | @type {Object} 133 | */ 134 | Object.defineProperty(DataStream.prototype, 'buffer', { 135 | get: function () { 136 | this._trimAlloc(); 137 | return this._buffer; 138 | }, 139 | set: function (v) { 140 | this._buffer = v; 141 | this._dataView = new DataView(this._buffer, this._byteOffset); 142 | this._byteLength = this._buffer.byteLength; 143 | }, 144 | }); 145 | 146 | /** 147 | Set/get the byteOffset of the DataStream object. 148 | The setter updates the DataView to point to the new byteOffset. 149 | @type {number} 150 | */ 151 | Object.defineProperty(DataStream.prototype, 'byteOffset', { 152 | get: function () { 153 | return this._byteOffset; 154 | }, 155 | set: function (v) { 156 | this._byteOffset = v; 157 | this._dataView = new DataView(this._buffer, this._byteOffset); 158 | this._byteLength = this._buffer.byteLength; 159 | }, 160 | }); 161 | 162 | /** 163 | Set/get the backing DataView of the DataStream object. 164 | The setter updates the buffer and byteOffset to point to the DataView values. 165 | @type {Object} 166 | */ 167 | Object.defineProperty(DataStream.prototype, 'dataView', { 168 | get: function () { 169 | return this._dataView; 170 | }, 171 | set: function (v) { 172 | this._byteOffset = v.byteOffset; 173 | this._buffer = v.buffer; 174 | this._dataView = new DataView(this._buffer, this._byteOffset); 175 | this._byteLength = this._byteOffset + v.byteLength; 176 | }, 177 | }); 178 | 179 | /** 180 | Internal function to resize the DataStream buffer when required. 181 | @param {number} extra Number of bytes to add to the buffer allocation. 182 | @return {null} 183 | */ 184 | DataStream.prototype._realloc = function (extra) { 185 | if (!this._dynamicSize) { 186 | return; 187 | } 188 | var req = this._byteOffset + this.position + extra; 189 | var blen = this._buffer.byteLength; 190 | if (req <= blen) { 191 | if (req > this._byteLength) { 192 | this._byteLength = req; 193 | } 194 | return; 195 | } 196 | if (blen < 1) { 197 | blen = 1; 198 | } 199 | while (req > blen) { 200 | blen *= 2; 201 | } 202 | var buf = new ArrayBuffer(blen); 203 | var src = new Uint8Array(this._buffer); 204 | var dst = new Uint8Array(buf, 0, src.length); 205 | dst.set(src); 206 | this.buffer = buf; 207 | this._byteLength = req; 208 | }; 209 | 210 | /** 211 | Internal function to trim the DataStream buffer when required. 212 | Used for stripping out the extra bytes from the backing buffer when 213 | the virtual byteLength is smaller than the buffer byteLength (happens after 214 | growing the buffer with writes and not filling the extra space completely). 215 | 216 | @return {null} 217 | */ 218 | DataStream.prototype._trimAlloc = function () { 219 | if (this._byteLength == this._buffer.byteLength) { 220 | return; 221 | } 222 | var buf = new ArrayBuffer(this._byteLength); 223 | var dst = new Uint8Array(buf); 224 | var src = new Uint8Array(this._buffer, 0, dst.length); 225 | dst.set(src); 226 | this.buffer = buf; 227 | }; 228 | 229 | /** 230 | Sets the DataStream read/write position to given position. 231 | Clamps between 0 and DataStream length. 232 | 233 | @param {number} pos Position to seek to. 234 | @return {null} 235 | */ 236 | DataStream.prototype.seek = function (pos) { 237 | var npos = Math.max(0, Math.min(this.byteLength, pos)); 238 | this.position = isNaN(npos) || !isFinite(npos) ? 0 : npos; 239 | }; 240 | 241 | /** 242 | Returns true if the DataStream seek pointer is at the end of buffer and 243 | there's no more data to read. 244 | 245 | @return {boolean} True if the seek pointer is at the end of the buffer. 246 | */ 247 | DataStream.prototype.isEof = function () { 248 | return this.position >= this.byteLength; 249 | }; 250 | 251 | /** 252 | Maps an Int32Array into the DataStream buffer, swizzling it to native 253 | endianness in-place. The current offset from the start of the buffer needs to 254 | be a multiple of element size, just like with typed array views. 255 | 256 | Nice for quickly reading in data. Warning: potentially modifies the buffer 257 | contents. 258 | 259 | @param {number} length Number of elements to map. 260 | @param {?boolean} e Endianness of the data to read. 261 | @return {Object} Int32Array to the DataStream backing buffer. 262 | */ 263 | DataStream.prototype.mapInt32Array = function (length, e) { 264 | this._realloc(length * 4); 265 | var arr = new Int32Array(this._buffer, this.byteOffset + this.position, length); 266 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 267 | this.position += length * 4; 268 | return arr; 269 | }; 270 | 271 | /** 272 | Maps an Int16Array into the DataStream buffer, swizzling it to native 273 | endianness in-place. The current offset from the start of the buffer needs to 274 | be a multiple of element size, just like with typed array views. 275 | 276 | Nice for quickly reading in data. Warning: potentially modifies the buffer 277 | contents. 278 | 279 | @param {number} length Number of elements to map. 280 | @param {?boolean} e Endianness of the data to read. 281 | @return {Object} Int16Array to the DataStream backing buffer. 282 | */ 283 | DataStream.prototype.mapInt16Array = function (length, e) { 284 | this._realloc(length * 2); 285 | var arr = new Int16Array(this._buffer, this.byteOffset + this.position, length); 286 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 287 | this.position += length * 2; 288 | return arr; 289 | }; 290 | 291 | /** 292 | Maps an Int8Array into the DataStream buffer. 293 | 294 | Nice for quickly reading in data. 295 | 296 | @param {number} length Number of elements to map. 297 | @param {?boolean} e Endianness of the data to read. 298 | @return {Object} Int8Array to the DataStream backing buffer. 299 | */ 300 | DataStream.prototype.mapInt8Array = function (length) { 301 | this._realloc(length * 1); 302 | var arr = new Int8Array(this._buffer, this.byteOffset + this.position, length); 303 | this.position += length * 1; 304 | return arr; 305 | }; 306 | 307 | /** 308 | Maps a Uint32Array into the DataStream buffer, swizzling it to native 309 | endianness in-place. The current offset from the start of the buffer needs to 310 | be a multiple of element size, just like with typed array views. 311 | 312 | Nice for quickly reading in data. Warning: potentially modifies the buffer 313 | contents. 314 | 315 | @param {number} length Number of elements to map. 316 | @param {?boolean} e Endianness of the data to read. 317 | @return {Object} Uint32Array to the DataStream backing buffer. 318 | */ 319 | DataStream.prototype.mapUint32Array = function (length, e) { 320 | this._realloc(length * 4); 321 | var arr = new Uint32Array(this._buffer, this.byteOffset + this.position, length); 322 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 323 | this.position += length * 4; 324 | return arr; 325 | }; 326 | 327 | /** 328 | Maps a Uint16Array into the DataStream buffer, swizzling it to native 329 | endianness in-place. The current offset from the start of the buffer needs to 330 | be a multiple of element size, just like with typed array views. 331 | 332 | Nice for quickly reading in data. Warning: potentially modifies the buffer 333 | contents. 334 | 335 | @param {number} length Number of elements to map. 336 | @param {?boolean} e Endianness of the data to read. 337 | @return {Object} Uint16Array to the DataStream backing buffer. 338 | */ 339 | DataStream.prototype.mapUint16Array = function (length, e) { 340 | this._realloc(length * 2); 341 | var arr = new Uint16Array(this._buffer, this.byteOffset + this.position, length); 342 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 343 | this.position += length * 2; 344 | return arr; 345 | }; 346 | 347 | /** 348 | Maps a Uint8Array into the DataStream buffer. 349 | 350 | Nice for quickly reading in data. 351 | 352 | @param {number} length Number of elements to map. 353 | @param {?boolean} e Endianness of the data to read. 354 | @return {Object} Uint8Array to the DataStream backing buffer. 355 | */ 356 | DataStream.prototype.mapUint8Array = function (length) { 357 | this._realloc(length * 1); 358 | var arr = new Uint8Array(this._buffer, this.byteOffset + this.position, length); 359 | this.position += length * 1; 360 | return arr; 361 | }; 362 | 363 | /** 364 | Maps a Float64Array into the DataStream buffer, swizzling it to native 365 | endianness in-place. The current offset from the start of the buffer needs to 366 | be a multiple of element size, just like with typed array views. 367 | 368 | Nice for quickly reading in data. Warning: potentially modifies the buffer 369 | contents. 370 | 371 | @param {number} length Number of elements to map. 372 | @param {?boolean} e Endianness of the data to read. 373 | @return {Object} Float64Array to the DataStream backing buffer. 374 | */ 375 | DataStream.prototype.mapFloat64Array = function (length, e) { 376 | this._realloc(length * 8); 377 | var arr = new Float64Array(this._buffer, this.byteOffset + this.position, length); 378 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 379 | this.position += length * 8; 380 | return arr; 381 | }; 382 | 383 | /** 384 | Maps a Float32Array into the DataStream buffer, swizzling it to native 385 | endianness in-place. The current offset from the start of the buffer needs to 386 | be a multiple of element size, just like with typed array views. 387 | 388 | Nice for quickly reading in data. Warning: potentially modifies the buffer 389 | contents. 390 | 391 | @param {number} length Number of elements to map. 392 | @param {?boolean} e Endianness of the data to read. 393 | @return {Object} Float32Array to the DataStream backing buffer. 394 | */ 395 | DataStream.prototype.mapFloat32Array = function (length, e) { 396 | this._realloc(length * 4); 397 | var arr = new Float32Array(this._buffer, this.byteOffset + this.position, length); 398 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 399 | this.position += length * 4; 400 | return arr; 401 | }; 402 | 403 | /** 404 | Reads an Int32Array of desired length and endianness from the DataStream. 405 | 406 | @param {number} length Number of elements to map. 407 | @param {?boolean} e Endianness of the data to read. 408 | @return {Object} The read Int32Array. 409 | */ 410 | DataStream.prototype.readInt32Array = function (length, e) { 411 | length = length == null ? this.byteLength - this.position / 4 : length; 412 | var arr = new Int32Array(length); 413 | DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); 414 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 415 | this.position += arr.byteLength; 416 | return arr; 417 | }; 418 | 419 | /** 420 | Reads an Int16Array of desired length and endianness from the DataStream. 421 | 422 | @param {number} length Number of elements to map. 423 | @param {?boolean} e Endianness of the data to read. 424 | @return {Object} The read Int16Array. 425 | */ 426 | DataStream.prototype.readInt16Array = function (length, e) { 427 | length = length == null ? this.byteLength - this.position / 2 : length; 428 | var arr = new Int16Array(length); 429 | DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); 430 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 431 | this.position += arr.byteLength; 432 | return arr; 433 | }; 434 | 435 | /** 436 | Reads an Int8Array of desired length from the DataStream. 437 | 438 | @param {number} length Number of elements to map. 439 | @param {?boolean} e Endianness of the data to read. 440 | @return {Object} The read Int8Array. 441 | */ 442 | DataStream.prototype.readInt8Array = function (length) { 443 | length = length == null ? this.byteLength - this.position : length; 444 | var arr = new Int8Array(length); 445 | DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); 446 | this.position += arr.byteLength; 447 | return arr; 448 | }; 449 | 450 | /** 451 | Reads a Uint32Array of desired length and endianness from the DataStream. 452 | 453 | @param {number} length Number of elements to map. 454 | @param {?boolean} e Endianness of the data to read. 455 | @return {Object} The read Uint32Array. 456 | */ 457 | DataStream.prototype.readUint32Array = function (length, e) { 458 | length = length == null ? this.byteLength - this.position / 4 : length; 459 | var arr = new Uint32Array(length); 460 | DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); 461 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 462 | this.position += arr.byteLength; 463 | return arr; 464 | }; 465 | 466 | /** 467 | Reads a Uint16Array of desired length and endianness from the DataStream. 468 | 469 | @param {number} length Number of elements to map. 470 | @param {?boolean} e Endianness of the data to read. 471 | @return {Object} The read Uint16Array. 472 | */ 473 | DataStream.prototype.readUint16Array = function (length, e) { 474 | length = length == null ? this.byteLength - this.position / 2 : length; 475 | var arr = new Uint16Array(length); 476 | DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); 477 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 478 | this.position += arr.byteLength; 479 | return arr; 480 | }; 481 | 482 | /** 483 | Reads a Uint8Array of desired length from the DataStream. 484 | 485 | @param {number} length Number of elements to map. 486 | @param {?boolean} e Endianness of the data to read. 487 | @return {Object} The read Uint8Array. 488 | */ 489 | DataStream.prototype.readUint8Array = function (length) { 490 | length = length == null ? this.byteLength - this.position : length; 491 | var arr = new Uint8Array(length); 492 | DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); 493 | this.position += arr.byteLength; 494 | return arr; 495 | }; 496 | 497 | /** 498 | Reads a Float64Array of desired length and endianness from the DataStream. 499 | 500 | @param {number} length Number of elements to map. 501 | @param {?boolean} e Endianness of the data to read. 502 | @return {Object} The read Float64Array. 503 | */ 504 | DataStream.prototype.readFloat64Array = function (length, e) { 505 | length = length == null ? this.byteLength - this.position / 8 : length; 506 | var arr = new Float64Array(length); 507 | DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); 508 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 509 | this.position += arr.byteLength; 510 | return arr; 511 | }; 512 | 513 | /** 514 | Reads a Float32Array of desired length and endianness from the DataStream. 515 | 516 | @param {number} length Number of elements to map. 517 | @param {?boolean} e Endianness of the data to read. 518 | @return {Object} The read Float32Array. 519 | */ 520 | DataStream.prototype.readFloat32Array = function (length, e) { 521 | length = length == null ? this.byteLength - this.position / 4 : length; 522 | var arr = new Float32Array(length); 523 | DataStream.memcpy(arr.buffer, 0, this.buffer, this.byteOffset + this.position, length * arr.BYTES_PER_ELEMENT); 524 | DataStream.arrayToNative(arr, e == null ? this.endianness : e); 525 | this.position += arr.byteLength; 526 | return arr; 527 | }; 528 | 529 | /** 530 | Writes an Int32Array of specified endianness to the DataStream. 531 | 532 | @param {Object} arr The array to write. 533 | @param {?boolean} e Endianness of the data to write. 534 | */ 535 | DataStream.prototype.writeInt32Array = function (arr, e) { 536 | this._realloc(arr.length * 4); 537 | if (arr instanceof Int32Array && this.byteOffset + (this.position % arr.BYTES_PER_ELEMENT) == 0) { 538 | DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, 0, arr.byteLength); 539 | this.mapInt32Array(arr.length, e); 540 | } else { 541 | for (var i = 0; i < arr.length; i++) { 542 | this.writeInt32(arr[i], e); 543 | } 544 | } 545 | }; 546 | 547 | /** 548 | Writes an Int16Array of specified endianness to the DataStream. 549 | 550 | @param {Object} arr The array to write. 551 | @param {?boolean} e Endianness of the data to write. 552 | */ 553 | DataStream.prototype.writeInt16Array = function (arr, e) { 554 | this._realloc(arr.length * 2); 555 | if (arr instanceof Int16Array && this.byteOffset + (this.position % arr.BYTES_PER_ELEMENT) == 0) { 556 | DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, 0, arr.byteLength); 557 | this.mapInt16Array(arr.length, e); 558 | } else { 559 | for (var i = 0; i < arr.length; i++) { 560 | this.writeInt16(arr[i], e); 561 | } 562 | } 563 | }; 564 | 565 | /** 566 | Writes an Int8Array to the DataStream. 567 | 568 | @param {Object} arr The array to write. 569 | */ 570 | DataStream.prototype.writeInt8Array = function (arr) { 571 | this._realloc(arr.length * 1); 572 | if (arr instanceof Int8Array && this.byteOffset + (this.position % arr.BYTES_PER_ELEMENT) == 0) { 573 | DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, 0, arr.byteLength); 574 | this.mapInt8Array(arr.length); 575 | } else { 576 | for (var i = 0; i < arr.length; i++) { 577 | this.writeInt8(arr[i]); 578 | } 579 | } 580 | }; 581 | 582 | /** 583 | Writes a Uint32Array of specified endianness to the DataStream. 584 | 585 | @param {Object} arr The array to write. 586 | @param {?boolean} e Endianness of the data to write. 587 | */ 588 | DataStream.prototype.writeUint32Array = function (arr, e) { 589 | this._realloc(arr.length * 4); 590 | if (arr instanceof Uint32Array && this.byteOffset + (this.position % arr.BYTES_PER_ELEMENT) == 0) { 591 | DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, 0, arr.byteLength); 592 | this.mapUint32Array(arr.length, e); 593 | } else { 594 | for (var i = 0; i < arr.length; i++) { 595 | this.writeUint32(arr[i], e); 596 | } 597 | } 598 | }; 599 | 600 | /** 601 | Writes a Uint16Array of specified endianness to the DataStream. 602 | 603 | @param {Object} arr The array to write. 604 | @param {?boolean} e Endianness of the data to write. 605 | */ 606 | DataStream.prototype.writeUint16Array = function (arr, e) { 607 | this._realloc(arr.length * 2); 608 | if (arr instanceof Uint16Array && this.byteOffset + (this.position % arr.BYTES_PER_ELEMENT) == 0) { 609 | DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, 0, arr.byteLength); 610 | this.mapUint16Array(arr.length, e); 611 | } else { 612 | for (var i = 0; i < arr.length; i++) { 613 | this.writeUint16(arr[i], e); 614 | } 615 | } 616 | }; 617 | 618 | /** 619 | Writes a Uint8Array to the DataStream. 620 | 621 | @param {Object} arr The array to write. 622 | */ 623 | DataStream.prototype.writeUint8Array = function (arr) { 624 | this._realloc(arr.length * 1); 625 | if (arr instanceof Uint8Array && this.byteOffset + (this.position % arr.BYTES_PER_ELEMENT) == 0) { 626 | DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, 0, arr.byteLength); 627 | this.mapUint8Array(arr.length); 628 | } else { 629 | for (var i = 0; i < arr.length; i++) { 630 | this.writeUint8(arr[i]); 631 | } 632 | } 633 | }; 634 | 635 | /** 636 | Writes a Float64Array of specified endianness to the DataStream. 637 | 638 | @param {Object} arr The array to write. 639 | @param {?boolean} e Endianness of the data to write. 640 | */ 641 | DataStream.prototype.writeFloat64Array = function (arr, e) { 642 | this._realloc(arr.length * 8); 643 | if (arr instanceof Float64Array && this.byteOffset + (this.position % arr.BYTES_PER_ELEMENT) == 0) { 644 | DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, 0, arr.byteLength); 645 | this.mapFloat64Array(arr.length, e); 646 | } else { 647 | for (var i = 0; i < arr.length; i++) { 648 | this.writeFloat64(arr[i], e); 649 | } 650 | } 651 | }; 652 | 653 | /** 654 | Writes a Float32Array of specified endianness to the DataStream. 655 | 656 | @param {Object} arr The array to write. 657 | @param {?boolean} e Endianness of the data to write. 658 | */ 659 | DataStream.prototype.writeFloat32Array = function (arr, e) { 660 | this._realloc(arr.length * 4); 661 | if (arr instanceof Float32Array && this.byteOffset + (this.position % arr.BYTES_PER_ELEMENT) == 0) { 662 | DataStream.memcpy(this._buffer, this.byteOffset + this.position, arr.buffer, 0, arr.byteLength); 663 | this.mapFloat32Array(arr.length, e); 664 | } else { 665 | for (var i = 0; i < arr.length; i++) { 666 | this.writeFloat32(arr[i], e); 667 | } 668 | } 669 | }; 670 | 671 | /** 672 | Reads a 32-bit int from the DataStream with the desired endianness. 673 | 674 | @param {?boolean} e Endianness of the number. 675 | @return {number} The read number. 676 | */ 677 | DataStream.prototype.readInt32 = function (e) { 678 | var v = this._dataView.getInt32(this.position, e == null ? this.endianness : e); 679 | this.position += 4; 680 | return v; 681 | }; 682 | 683 | /** 684 | Reads a 32-bit int from the DataStream with the offset. 685 | 686 | @param {number} offset The offset. 687 | @return {number} The read number. 688 | */ 689 | DataStream.prototype.readInt = function (offset) { 690 | this.seek(offset); 691 | return this.readInt32(); 692 | }; 693 | 694 | /** 695 | Reads a 16-bit int from the DataStream with the desired endianness. 696 | 697 | @param {?boolean} e Endianness of the number. 698 | @return {number} The read number. 699 | */ 700 | DataStream.prototype.readInt16 = function (e) { 701 | var v = this._dataView.getInt16(this.position, e == null ? this.endianness : e); 702 | this.position += 2; 703 | return v; 704 | }; 705 | 706 | /** 707 | Reads a 16-bit int from the DataStream with the offset 708 | 709 | @param {number} offset The offset. 710 | @return {number} The read number. 711 | */ 712 | DataStream.prototype.readShort = function (offset) { 713 | this.seek(offset); 714 | return this.readInt16(); 715 | }; 716 | 717 | /** 718 | Reads an 8-bit int from the DataStream. 719 | 720 | @return {number} The read number. 721 | */ 722 | DataStream.prototype.readInt8 = function () { 723 | var v = this._dataView.getInt8(this.position); 724 | this.position += 1; 725 | return v; 726 | }; 727 | 728 | /** 729 | Reads an 8-bit int from the DataStream with the offset. 730 | 731 | @param {number} offset The offset. 732 | @return {number} The read number. 733 | */ 734 | DataStream.prototype.readByte = function (offset) { 735 | this.seek(offset); 736 | return this.readInt8(); 737 | }; 738 | 739 | /** 740 | Reads a 32-bit unsigned int from the DataStream with the desired endianness. 741 | 742 | @param {?boolean} e Endianness of the number. 743 | @return {number} The read number. 744 | */ 745 | DataStream.prototype.readUint32 = function (e) { 746 | var v = this._dataView.getUint32(this.position, e == null ? this.endianness : e); 747 | this.position += 4; 748 | return v; 749 | }; 750 | 751 | /** 752 | Reads a 16-bit unsigned int from the DataStream with the desired endianness. 753 | 754 | @param {?boolean} e Endianness of the number. 755 | @return {number} The read number. 756 | */ 757 | DataStream.prototype.readUint16 = function (e) { 758 | var v = this._dataView.getUint16(this.position, e == null ? this.endianness : e); 759 | this.position += 2; 760 | return v; 761 | }; 762 | 763 | /** 764 | Reads an 8-bit unsigned int from the DataStream. 765 | 766 | @return {number} The read number. 767 | */ 768 | DataStream.prototype.readUint8 = function () { 769 | var v = this._dataView.getUint8(this.position); 770 | this.position += 1; 771 | return v; 772 | }; 773 | 774 | /** 775 | Reads a 32-bit float from the DataStream with the desired endianness. 776 | 777 | @param {?boolean} e Endianness of the number. 778 | @return {number} The read number. 779 | */ 780 | DataStream.prototype.readFloat32 = function (e) { 781 | var v = this._dataView.getFloat32(this.position, e == null ? this.endianness : e); 782 | this.position += 4; 783 | return v; 784 | }; 785 | 786 | /** 787 | Reads a 64-bit float from the DataStream with the desired endianness. 788 | 789 | @param {?boolean} e Endianness of the number. 790 | @return {number} The read number. 791 | */ 792 | DataStream.prototype.readFloat64 = function (e) { 793 | var v = this._dataView.getFloat64(this.position, e == null ? this.endianness : e); 794 | this.position += 8; 795 | return v; 796 | }; 797 | 798 | /** 799 | Writes a 32-bit int to the DataStream with the desired endianness. 800 | 801 | @param {number} v Number to write. 802 | @param {?boolean} e Endianness of the number. 803 | */ 804 | DataStream.prototype.writeInt32 = function (v, e) { 805 | this._realloc(4); 806 | this._dataView.setInt32(this.position, v, e == null ? this.endianness : e); 807 | this.position += 4; 808 | }; 809 | 810 | /** 811 | Writes a 16-bit int to the DataStream with the desired endianness. 812 | 813 | @param {number} v Number to write. 814 | @param {?boolean} e Endianness of the number. 815 | */ 816 | DataStream.prototype.writeInt16 = function (v, e) { 817 | this._realloc(2); 818 | this._dataView.setInt16(this.position, v, e == null ? this.endianness : e); 819 | this.position += 2; 820 | }; 821 | 822 | /** 823 | Writes an 8-bit int to the DataStream. 824 | 825 | @param {number} v Number to write. 826 | */ 827 | DataStream.prototype.writeInt8 = function (v) { 828 | this._realloc(1); 829 | this._dataView.setInt8(this.position, v); 830 | this.position += 1; 831 | }; 832 | 833 | /** 834 | Writes a 32-bit unsigned int to the DataStream with the desired endianness. 835 | 836 | @param {number} v Number to write. 837 | @param {?boolean} e Endianness of the number. 838 | */ 839 | DataStream.prototype.writeUint32 = function (v, e) { 840 | this._realloc(4); 841 | this._dataView.setUint32(this.position, v, e == null ? this.endianness : e); 842 | this.position += 4; 843 | }; 844 | 845 | /** 846 | Writes a 16-bit unsigned int to the DataStream with the desired endianness. 847 | 848 | @param {number} v Number to write. 849 | @param {?boolean} e Endianness of the number. 850 | */ 851 | DataStream.prototype.writeUint16 = function (v, e) { 852 | this._realloc(2); 853 | this._dataView.setUint16(this.position, v, e == null ? this.endianness : e); 854 | this.position += 2; 855 | }; 856 | 857 | /** 858 | Writes an 8-bit unsigned int to the DataStream. 859 | 860 | @param {number} v Number to write. 861 | */ 862 | DataStream.prototype.writeUint8 = function (v) { 863 | this._realloc(1); 864 | this._dataView.setUint8(this.position, v); 865 | this.position += 1; 866 | }; 867 | 868 | /** 869 | Writes a 32-bit float to the DataStream with the desired endianness. 870 | 871 | @param {number} v Number to write. 872 | @param {?boolean} e Endianness of the number. 873 | */ 874 | DataStream.prototype.writeFloat32 = function (v, e) { 875 | this._realloc(4); 876 | this._dataView.setFloat32(this.position, v, e == null ? this.endianness : e); 877 | this.position += 4; 878 | }; 879 | 880 | /** 881 | Writes a 64-bit float to the DataStream with the desired endianness. 882 | 883 | @param {number} v Number to write. 884 | @param {?boolean} e Endianness of the number. 885 | */ 886 | DataStream.prototype.writeFloat64 = function (v, e) { 887 | this._realloc(8); 888 | this._dataView.setFloat64(this.position, v, e == null ? this.endianness : e); 889 | this.position += 8; 890 | }; 891 | 892 | /** 893 | Native endianness. Either DataStream.BIG_ENDIAN or DataStream.LITTLE_ENDIAN 894 | depending on the platform endianness. 895 | 896 | @type {boolean} 897 | */ 898 | DataStream.endianness = new Int8Array(new Int16Array([1]).buffer)[0] > 0; 899 | 900 | /** 901 | Copies byteLength bytes from the src buffer at srcOffset to the 902 | dst buffer at dstOffset. 903 | 904 | @param {Object} dst Destination ArrayBuffer to write to. 905 | @param {number} dstOffset Offset to the destination ArrayBuffer. 906 | @param {Object} src Source ArrayBuffer to read from. 907 | @param {number} srcOffset Offset to the source ArrayBuffer. 908 | @param {number} byteLength Number of bytes to copy. 909 | */ 910 | DataStream.memcpy = function (dst, dstOffset, src, srcOffset, byteLength) { 911 | var dstU8 = new Uint8Array(dst, dstOffset, byteLength); 912 | var srcU8 = new Uint8Array(src, srcOffset, byteLength); 913 | dstU8.set(srcU8); 914 | }; 915 | 916 | /** 917 | Converts array to native endianness in-place. 918 | 919 | @param {Object} array Typed array to convert. 920 | @param {boolean} arrayIsLittleEndian True if the data in the array is 921 | little-endian. Set false for big-endian. 922 | @return {Object} The converted typed array. 923 | */ 924 | DataStream.arrayToNative = function (array, arrayIsLittleEndian) { 925 | if (arrayIsLittleEndian == this.endianness) { 926 | return array; 927 | } else { 928 | return this.flipArrayEndianness(array); 929 | } 930 | }; 931 | 932 | /** 933 | Converts native endianness array to desired endianness in-place. 934 | 935 | @param {Object} array Typed array to convert. 936 | @param {boolean} littleEndian True if the converted array should be 937 | little-endian. Set false for big-endian. 938 | @return {Object} The converted typed array. 939 | */ 940 | DataStream.nativeToEndian = function (array, littleEndian) { 941 | if (this.endianness == littleEndian) { 942 | return array; 943 | } else { 944 | return this.flipArrayEndianness(array); 945 | } 946 | }; 947 | 948 | /** 949 | Flips typed array endianness in-place. 950 | 951 | @param {Object} array Typed array to flip. 952 | @return {Object} The converted typed array. 953 | */ 954 | DataStream.flipArrayEndianness = function (array) { 955 | var u8 = new Uint8Array(array.buffer, array.byteOffset, array.byteLength); 956 | for (var i = 0; i < array.byteLength; i += array.BYTES_PER_ELEMENT) { 957 | for (var j = i + array.BYTES_PER_ELEMENT - 1, k = i; j > k; j--, k++) { 958 | var tmp = u8[k]; 959 | u8[k] = u8[j]; 960 | u8[j] = tmp; 961 | } 962 | } 963 | return array; 964 | }; 965 | 966 | /** 967 | Creates an array from an array of character codes. 968 | Uses String.fromCharCode on the character codes and concats the results into a string. 969 | 970 | @param {array} array Array of character codes. 971 | @return {string} String created from the character codes. 972 | **/ 973 | DataStream.createStringFromArray = function (array) { 974 | var str = ''; 975 | for (var i = 0; i < array.length; i++) { 976 | str += String.fromCharCode(array[i]); 977 | } 978 | return str; 979 | }; 980 | 981 | /** 982 | Seek position where DataStream#readStruct ran into a problem. 983 | Useful for debugging struct parsing. 984 | 985 | @type {number} 986 | */ 987 | DataStream.prototype.failurePosition = 0; 988 | 989 | /** 990 | Reads a struct of data from the DataStream. The struct is defined as 991 | a flat array of [name, type]-pairs. See the example below: 992 | 993 | ds.readStruct([ 994 | 'headerTag', 'uint32', // Uint32 in DataStream endianness. 995 | 'headerTag2', 'uint32be', // Big-endian Uint32. 996 | 'headerTag3', 'uint32le', // Little-endian Uint32. 997 | 'array', ['[]', 'uint32', 16], // Uint32Array of length 16. 998 | 'array2Length', 'uint32', 999 | 'array2', ['[]', 'uint32', 'array2Length'] // Uint32Array of length array2Length 1000 | ]); 1001 | 1002 | The possible values for the type are as follows: 1003 | 1004 | // Number types 1005 | 1006 | // Unsuffixed number types use DataStream endianness. 1007 | // To explicitly specify endianness, suffix the type with 1008 | // 'le' for little-endian or 'be' for big-endian, 1009 | // e.g. 'int32be' for big-endian int32. 1010 | 1011 | 'uint8' -- 8-bit unsigned int 1012 | 'uint16' -- 16-bit unsigned int 1013 | 'uint32' -- 32-bit unsigned int 1014 | 'int8' -- 8-bit int 1015 | 'int16' -- 16-bit int 1016 | 'int32' -- 32-bit int 1017 | 'float32' -- 32-bit float 1018 | 'float64' -- 64-bit float 1019 | 1020 | // String types 1021 | 'cstring' -- ASCII string terminated by a zero byte. 1022 | 'string:N' -- ASCII string of length N, where N is a literal integer. 1023 | 'string:variableName' -- ASCII string of length $variableName, 1024 | where 'variableName' is a previously parsed number in the current struct. 1025 | 'string,CHARSET:N' -- String of byteLength N encoded with given CHARSET. 1026 | 'u16string:N' -- UCS-2 string of length N in DataStream endianness. 1027 | 'u16stringle:N' -- UCS-2 string of length N in little-endian. 1028 | 'u16stringbe:N' -- UCS-2 string of length N in big-endian. 1029 | 1030 | // Complex types 1031 | [name, type, name_2, type_2, ..., name_N, type_N] -- Struct 1032 | function(dataStream, struct) {} -- Callback function to read and return data. 1033 | {get: function(dataStream, struct) {}, 1034 | set: function(dataStream, struct) {}} 1035 | -- Getter/setter functions to read and return data, handy for using the same 1036 | struct definition for reading and writing structs. 1037 | ['[]', type, length] -- Array of given type and length. The length can be either 1038 | a number, a string that references a previously-read 1039 | field, or a callback function(struct, dataStream, type){}. 1040 | If length is '*', reads in as many elements as it can. 1041 | 1042 | @param {Object} structDefinition Struct definition object. 1043 | @return {Object} The read struct. Null if failed to read struct. 1044 | */ 1045 | DataStream.prototype.readStruct = function (structDefinition) { 1046 | var struct = {}, 1047 | t, 1048 | v, 1049 | n; 1050 | var p = this.position; 1051 | for (var i = 0; i < structDefinition.length; i += 2) { 1052 | t = structDefinition[i + 1]; 1053 | v = this.readType(t, struct); 1054 | if (v == null) { 1055 | if (this.failurePosition == 0) { 1056 | this.failurePosition = this.position; 1057 | } 1058 | this.position = p; 1059 | return null; 1060 | } 1061 | struct[structDefinition[i]] = v; 1062 | } 1063 | return struct; 1064 | }; 1065 | 1066 | /** 1067 | Read UCS-2 string of desired length and endianness from the DataStream. 1068 | 1069 | @param {number} length The length of the string to read. 1070 | @param {boolean} endianness The endianness of the string data in the DataStream. 1071 | @return {string} The read string. 1072 | */ 1073 | DataStream.prototype.readUCS2String = function (length, endianness) { 1074 | return DataStream.createStringFromArray(this.readUint16Array(length, endianness)); 1075 | }; 1076 | 1077 | /** 1078 | Read UCS-2 string of desired length and offset from the DataStream. 1079 | 1080 | @param {number} offset The offset. 1081 | @param {number} length The length of the string to read. 1082 | @return {string} The read string. 1083 | */ 1084 | DataStream.prototype.readStringAt = function (offset, length) { 1085 | this.seek(offset); 1086 | return this.readUCS2String(length); 1087 | }; 1088 | 1089 | /** 1090 | Write a UCS-2 string of desired endianness to the DataStream. The 1091 | lengthOverride argument lets you define the number of characters to write. 1092 | If the string is shorter than lengthOverride, the extra space is padded with 1093 | zeroes. 1094 | 1095 | @param {string} str The string to write. 1096 | @param {?boolean} endianness The endianness to use for the written string data. 1097 | @param {?number} lengthOverride The number of characters to write. 1098 | */ 1099 | DataStream.prototype.writeUCS2String = function (str, endianness, lengthOverride) { 1100 | if (lengthOverride == null) { 1101 | lengthOverride = str.length; 1102 | } 1103 | for (var i = 0; i < str.length && i < lengthOverride; i++) { 1104 | this.writeUint16(str.charCodeAt(i), endianness); 1105 | } 1106 | for (; i < lengthOverride; i++) { 1107 | this.writeUint16(0); 1108 | } 1109 | }; 1110 | 1111 | /** 1112 | Read a string of desired length and encoding from the DataStream. 1113 | 1114 | @param {number} length The length of the string to read in bytes. 1115 | @param {?string} encoding The encoding of the string data in the DataStream. 1116 | Defaults to ASCII. 1117 | @return {string} The read string. 1118 | */ 1119 | DataStream.prototype.readString = function (length, encoding) { 1120 | if (encoding == null || encoding == 'ASCII') { 1121 | return DataStream.createStringFromArray( 1122 | this.mapUint8Array(length == null ? this.byteLength - this.position : length) 1123 | ); 1124 | } else { 1125 | return new TextDecoder(encoding).decode(this.mapUint8Array(length)); 1126 | } 1127 | }; 1128 | 1129 | /** 1130 | Writes a string of desired length and encoding to the DataStream. 1131 | 1132 | @param {string} s The string to write. 1133 | @param {?string} encoding The encoding for the written string data. 1134 | Defaults to ASCII. 1135 | @param {?number} length The number of characters to write. 1136 | */ 1137 | DataStream.prototype.writeString = function (s, encoding, length) { 1138 | if (encoding == null || encoding == 'ASCII') { 1139 | if (length != null) { 1140 | var i = 0; 1141 | var len = Math.min(s.length, length); 1142 | for (i = 0; i < len; i++) { 1143 | this.writeUint8(s.charCodeAt(i)); 1144 | } 1145 | for (; i < length; i++) { 1146 | this.writeUint8(0); 1147 | } 1148 | } else { 1149 | for (var i = 0; i < s.length; i++) { 1150 | this.writeUint8(s.charCodeAt(i)); 1151 | } 1152 | } 1153 | } else { 1154 | this.writeUint8Array(new TextEncoder(encoding).encode(s.substring(0, length))); 1155 | } 1156 | }; 1157 | 1158 | /** 1159 | Read null-terminated string of desired length from the DataStream. Truncates 1160 | the returned string so that the null byte is not a part of it. 1161 | 1162 | @param {?number} length The length of the string to read. 1163 | @return {string} The read string. 1164 | */ 1165 | DataStream.prototype.readCString = function (length) { 1166 | var blen = this.byteLength - this.position; 1167 | var u8 = new Uint8Array(this._buffer, this._byteOffset + this.position); 1168 | var len = blen; 1169 | if (length != null) { 1170 | len = Math.min(length, blen); 1171 | } 1172 | for (var i = 0; i < len && u8[i] != 0; i++); // find first zero byte 1173 | var s = DataStream.createStringFromArray(this.mapUint8Array(i)); 1174 | if (length != null) { 1175 | this.position += len - i; 1176 | } else if (i != blen) { 1177 | this.position += 1; // trailing zero if not at end of buffer 1178 | } 1179 | return s; 1180 | }; 1181 | 1182 | /** 1183 | Writes a null-terminated string to DataStream and zero-pads it to length 1184 | bytes. If length is not given, writes the string followed by a zero. 1185 | If string is longer than length, the written part of the string does not have 1186 | a trailing zero. 1187 | 1188 | @param {string} s The string to write. 1189 | @param {?number} length The number of characters to write. 1190 | */ 1191 | DataStream.prototype.writeCString = function (s, length) { 1192 | if (length != null) { 1193 | var i = 0; 1194 | var len = Math.min(s.length, length); 1195 | for (i = 0; i < len; i++) { 1196 | this.writeUint8(s.charCodeAt(i)); 1197 | } 1198 | for (; i < length; i++) { 1199 | this.writeUint8(0); 1200 | } 1201 | } else { 1202 | for (var i = 0; i < s.length; i++) { 1203 | this.writeUint8(s.charCodeAt(i)); 1204 | } 1205 | this.writeUint8(0); 1206 | } 1207 | }; 1208 | 1209 | /** 1210 | Reads an object of type t from the DataStream, passing struct as the thus-far 1211 | read struct to possible callbacks that refer to it. Used by readStruct for 1212 | reading in the values, so the type is one of the readStruct types. 1213 | 1214 | @param {Object} t Type of the object to read. 1215 | @param {?Object} struct Struct to refer to when resolving length references 1216 | and for calling callbacks. 1217 | @return {?Object} Returns the object on successful read, null on unsuccessful. 1218 | */ 1219 | DataStream.prototype.readType = function (t, struct) { 1220 | if (typeof t == 'function') { 1221 | return t(this, struct); 1222 | } else if (typeof t == 'object' && !(t instanceof Array)) { 1223 | return t.get(this, struct); 1224 | } else if (t instanceof Array && t.length != 3) { 1225 | return this.readStruct(t, struct); 1226 | } 1227 | var v = null; 1228 | var lengthOverride = null; 1229 | var charset = 'ASCII'; 1230 | var pos = this.position; 1231 | var len; 1232 | if (typeof t == 'string' && /:/.test(t)) { 1233 | var tp = t.split(':'); 1234 | t = tp[0]; 1235 | len = tp[1]; 1236 | 1237 | // allow length to be previously parsed variable 1238 | // e.g. 'string:fieldLength', if `fieldLength` has 1239 | // been parsed previously. 1240 | if (struct[len] != null) { 1241 | lengthOverride = parseInt(struct[len]); 1242 | } else { 1243 | // assume literal integer e.g., 'string:4' 1244 | lengthOverride = parseInt(tp[1]); 1245 | } 1246 | } 1247 | if (typeof t == 'string' && /,/.test(t)) { 1248 | var tp = t.split(','); 1249 | t = tp[0]; 1250 | charset = parseInt(tp[1]); 1251 | } 1252 | switch (t) { 1253 | case 'uint8': 1254 | v = this.readUint8(); 1255 | break; 1256 | case 'int8': 1257 | v = this.readInt8(); 1258 | break; 1259 | 1260 | case 'uint16': 1261 | v = this.readUint16(this.endianness); 1262 | break; 1263 | case 'int16': 1264 | v = this.readInt16(this.endianness); 1265 | break; 1266 | case 'uint32': 1267 | v = this.readUint32(this.endianness); 1268 | break; 1269 | case 'int32': 1270 | v = this.readInt32(this.endianness); 1271 | break; 1272 | case 'float32': 1273 | v = this.readFloat32(this.endianness); 1274 | break; 1275 | case 'float64': 1276 | v = this.readFloat64(this.endianness); 1277 | break; 1278 | 1279 | case 'uint16be': 1280 | v = this.readUint16(DataStream.BIG_ENDIAN); 1281 | break; 1282 | case 'int16be': 1283 | v = this.readInt16(DataStream.BIG_ENDIAN); 1284 | break; 1285 | case 'uint32be': 1286 | v = this.readUint32(DataStream.BIG_ENDIAN); 1287 | break; 1288 | case 'int32be': 1289 | v = this.readInt32(DataStream.BIG_ENDIAN); 1290 | break; 1291 | case 'float32be': 1292 | v = this.readFloat32(DataStream.BIG_ENDIAN); 1293 | break; 1294 | case 'float64be': 1295 | v = this.readFloat64(DataStream.BIG_ENDIAN); 1296 | break; 1297 | 1298 | case 'uint16le': 1299 | v = this.readUint16(DataStream.LITTLE_ENDIAN); 1300 | break; 1301 | case 'int16le': 1302 | v = this.readInt16(DataStream.LITTLE_ENDIAN); 1303 | break; 1304 | case 'uint32le': 1305 | v = this.readUint32(DataStream.LITTLE_ENDIAN); 1306 | break; 1307 | case 'int32le': 1308 | v = this.readInt32(DataStream.LITTLE_ENDIAN); 1309 | break; 1310 | case 'float32le': 1311 | v = this.readFloat32(DataStream.LITTLE_ENDIAN); 1312 | break; 1313 | case 'float64le': 1314 | v = this.readFloat64(DataStream.LITTLE_ENDIAN); 1315 | break; 1316 | 1317 | case 'cstring': 1318 | v = this.readCString(lengthOverride); 1319 | break; 1320 | 1321 | case 'string': 1322 | v = this.readString(lengthOverride, charset); 1323 | break; 1324 | 1325 | case 'u16string': 1326 | v = this.readUCS2String(lengthOverride, this.endianness); 1327 | break; 1328 | 1329 | case 'u16stringle': 1330 | v = this.readUCS2String(lengthOverride, DataStream.LITTLE_ENDIAN); 1331 | break; 1332 | 1333 | case 'u16stringbe': 1334 | v = this.readUCS2String(lengthOverride, DataStream.BIG_ENDIAN); 1335 | break; 1336 | 1337 | default: 1338 | if (t.length == 3) { 1339 | var ta = t[1]; 1340 | var len = t[2]; 1341 | var length = 0; 1342 | if (typeof len == 'function') { 1343 | length = len(struct, this, t); 1344 | } else if (typeof len == 'string' && struct[len] != null) { 1345 | length = parseInt(struct[len]); 1346 | } else { 1347 | length = parseInt(len); 1348 | } 1349 | if (typeof ta == 'string') { 1350 | var tap = ta.replace(/(le|be)$/, ''); 1351 | var endianness = null; 1352 | if (/le$/.test(ta)) { 1353 | endianness = DataStream.LITTLE_ENDIAN; 1354 | } else if (/be$/.test(ta)) { 1355 | endianness = DataStream.BIG_ENDIAN; 1356 | } 1357 | if (len == '*') { 1358 | length = null; 1359 | } 1360 | switch (tap) { 1361 | case 'uint8': 1362 | v = this.readUint8Array(length); 1363 | break; 1364 | case 'uint16': 1365 | v = this.readUint16Array(length, endianness); 1366 | break; 1367 | case 'uint32': 1368 | v = this.readUint32Array(length, endianness); 1369 | break; 1370 | case 'int8': 1371 | v = this.readInt8Array(length); 1372 | break; 1373 | case 'int16': 1374 | v = this.readInt16Array(length, endianness); 1375 | break; 1376 | case 'int32': 1377 | v = this.readInt32Array(length, endianness); 1378 | break; 1379 | case 'float32': 1380 | v = this.readFloat32Array(length, endianness); 1381 | break; 1382 | case 'float64': 1383 | v = this.readFloat64Array(length, endianness); 1384 | break; 1385 | case 'cstring': 1386 | case 'utf16string': 1387 | case 'string': 1388 | if (length == null) { 1389 | v = []; 1390 | while (!this.isEof()) { 1391 | var u = this.readType(ta, struct); 1392 | if (u == null) break; 1393 | v.push(u); 1394 | } 1395 | } else { 1396 | v = new Array(length); 1397 | for (var i = 0; i < length; i++) { 1398 | v[i] = this.readType(ta, struct); 1399 | } 1400 | } 1401 | break; 1402 | } 1403 | } else { 1404 | if (len == '*') { 1405 | v = []; 1406 | this.buffer; 1407 | while (true) { 1408 | var p = this.position; 1409 | try { 1410 | var o = this.readType(ta, struct); 1411 | if (o == null) { 1412 | this.position = p; 1413 | break; 1414 | } 1415 | v.push(o); 1416 | } catch (e) { 1417 | this.position = p; 1418 | break; 1419 | } 1420 | } 1421 | } else { 1422 | v = new Array(length); 1423 | for (var i = 0; i < length; i++) { 1424 | var u = this.readType(ta, struct); 1425 | if (u == null) return null; 1426 | v[i] = u; 1427 | } 1428 | } 1429 | } 1430 | break; 1431 | } 1432 | } 1433 | if (lengthOverride != null) { 1434 | this.position = pos + lengthOverride; 1435 | } 1436 | return v; 1437 | }; 1438 | 1439 | /** 1440 | Writes a struct to the DataStream. Takes a structDefinition that gives the 1441 | types and a struct object that gives the values. Refer to readStruct for the 1442 | structure of structDefinition. 1443 | 1444 | @param {Object} structDefinition Type definition of the struct. 1445 | @param {Object} struct The struct data object. 1446 | */ 1447 | DataStream.prototype.writeStruct = function (structDefinition, struct) { 1448 | for (var i = 0; i < structDefinition.length; i += 2) { 1449 | var t = structDefinition[i + 1]; 1450 | this.writeType(t, struct[structDefinition[i]], struct); 1451 | } 1452 | }; 1453 | 1454 | /** 1455 | Writes object v of type t to the DataStream. 1456 | 1457 | @param {Object} t Type of data to write. 1458 | @param {Object} v Value of data to write. 1459 | @param {Object} struct Struct to pass to write callback functions. 1460 | */ 1461 | DataStream.prototype.writeType = function (t, v, struct) { 1462 | if (typeof t == 'function') { 1463 | return t(this, v); 1464 | } else if (typeof t == 'object' && !(t instanceof Array)) { 1465 | return t.set(this, v, struct); 1466 | } 1467 | var lengthOverride = null; 1468 | var charset = 'ASCII'; 1469 | var pos = this.position; 1470 | if (typeof t == 'string' && /:/.test(t)) { 1471 | var tp = t.split(':'); 1472 | t = tp[0]; 1473 | lengthOverride = parseInt(tp[1]); 1474 | } 1475 | if (typeof t == 'string' && /,/.test(t)) { 1476 | var tp = t.split(','); 1477 | t = tp[0]; 1478 | charset = parseInt(tp[1]); 1479 | } 1480 | 1481 | switch (t) { 1482 | case 'uint8': 1483 | this.writeUint8(v); 1484 | break; 1485 | case 'int8': 1486 | this.writeInt8(v); 1487 | break; 1488 | 1489 | case 'uint16': 1490 | this.writeUint16(v, this.endianness); 1491 | break; 1492 | case 'int16': 1493 | this.writeInt16(v, this.endianness); 1494 | break; 1495 | case 'uint32': 1496 | this.writeUint32(v, this.endianness); 1497 | break; 1498 | case 'int32': 1499 | this.writeInt32(v, this.endianness); 1500 | break; 1501 | case 'float32': 1502 | this.writeFloat32(v, this.endianness); 1503 | break; 1504 | case 'float64': 1505 | this.writeFloat64(v, this.endianness); 1506 | break; 1507 | 1508 | case 'uint16be': 1509 | this.writeUint16(v, DataStream.BIG_ENDIAN); 1510 | break; 1511 | case 'int16be': 1512 | this.writeInt16(v, DataStream.BIG_ENDIAN); 1513 | break; 1514 | case 'uint32be': 1515 | this.writeUint32(v, DataStream.BIG_ENDIAN); 1516 | break; 1517 | case 'int32be': 1518 | this.writeInt32(v, DataStream.BIG_ENDIAN); 1519 | break; 1520 | case 'float32be': 1521 | this.writeFloat32(v, DataStream.BIG_ENDIAN); 1522 | break; 1523 | case 'float64be': 1524 | this.writeFloat64(v, DataStream.BIG_ENDIAN); 1525 | break; 1526 | 1527 | case 'uint16le': 1528 | this.writeUint16(v, DataStream.LITTLE_ENDIAN); 1529 | break; 1530 | case 'int16le': 1531 | this.writeInt16(v, DataStream.LITTLE_ENDIAN); 1532 | break; 1533 | case 'uint32le': 1534 | this.writeUint32(v, DataStream.LITTLE_ENDIAN); 1535 | break; 1536 | case 'int32le': 1537 | this.writeInt32(v, DataStream.LITTLE_ENDIAN); 1538 | break; 1539 | case 'float32le': 1540 | this.writeFloat32(v, DataStream.LITTLE_ENDIAN); 1541 | break; 1542 | case 'float64le': 1543 | this.writeFloat64(v, DataStream.LITTLE_ENDIAN); 1544 | break; 1545 | 1546 | case 'cstring': 1547 | this.writeCString(v, lengthOverride); 1548 | break; 1549 | 1550 | case 'string': 1551 | this.writeString(v, charset, lengthOverride); 1552 | break; 1553 | 1554 | case 'u16string': 1555 | this.writeUCS2String(v, this.endianness, lengthOverride); 1556 | break; 1557 | 1558 | case 'u16stringle': 1559 | this.writeUCS2String(v, DataStream.LITTLE_ENDIAN, lengthOverride); 1560 | break; 1561 | 1562 | case 'u16stringbe': 1563 | this.writeUCS2String(v, DataStream.BIG_ENDIAN, lengthOverride); 1564 | break; 1565 | 1566 | default: 1567 | if (t.length == 3) { 1568 | var ta = t[1]; 1569 | for (var i = 0; i < v.length; i++) { 1570 | this.writeType(ta, v[i]); 1571 | } 1572 | break; 1573 | } else { 1574 | this.writeStruct(t, v); 1575 | break; 1576 | } 1577 | } 1578 | if (lengthOverride != null) { 1579 | this.position = pos; 1580 | this._realloc(lengthOverride); 1581 | this.position = pos + lengthOverride; 1582 | } 1583 | }; 1584 | 1585 | // Export DataStream for amd environments 1586 | if (typeof define === 'function' && define.amd) { 1587 | define('DataStream', [], function () { 1588 | return DataStream; 1589 | }); 1590 | } 1591 | 1592 | /* -----------------------------------------------m*/ 1593 | 1594 | // constants 1595 | var CONST = { 1596 | FILE_HEADER: uInt2int([0xd0, 0xcf, 0x11, 0xe0, 0xa1, 0xb1, 0x1a, 0xe1]), 1597 | MSG: { 1598 | UNUSED_BLOCK: -1, 1599 | END_OF_CHAIN: -2, 1600 | 1601 | S_BIG_BLOCK_SIZE: 0x0200, 1602 | S_BIG_BLOCK_MARK: 9, 1603 | 1604 | L_BIG_BLOCK_SIZE: 0x1000, 1605 | L_BIG_BLOCK_MARK: 12, 1606 | 1607 | SMALL_BLOCK_SIZE: 0x0040, 1608 | BIG_BLOCK_MIN_DOC_SIZE: 0x1000, 1609 | HEADER: { 1610 | PROPERTY_START_OFFSET: 0x30, 1611 | 1612 | BAT_START_OFFSET: 0x4c, 1613 | BAT_COUNT_OFFSET: 0x2c, 1614 | 1615 | SBAT_START_OFFSET: 0x3c, 1616 | SBAT_COUNT_OFFSET: 0x40, 1617 | 1618 | XBAT_START_OFFSET: 0x44, 1619 | XBAT_COUNT_OFFSET: 0x48, 1620 | }, 1621 | PROP: { 1622 | NO_INDEX: -1, 1623 | PROPERTY_SIZE: 0x0080, 1624 | 1625 | NAME_SIZE_OFFSET: 0x40, 1626 | MAX_NAME_LENGTH: /*NAME_SIZE_OFFSET*/ 0x40 / 2 - 1, 1627 | TYPE_OFFSET: 0x42, 1628 | PREVIOUS_PROPERTY_OFFSET: 0x44, 1629 | NEXT_PROPERTY_OFFSET: 0x48, 1630 | CHILD_PROPERTY_OFFSET: 0x4c, 1631 | START_BLOCK_OFFSET: 0x74, 1632 | SIZE_OFFSET: 0x78, 1633 | TYPE_ENUM: { 1634 | DIRECTORY: 1, 1635 | DOCUMENT: 2, 1636 | ROOT: 5, 1637 | }, 1638 | }, 1639 | FIELD: { 1640 | PREFIX: { 1641 | ATTACHMENT: '__attach_version1.0', 1642 | RECIPIENT: '__recip_version1.0', 1643 | DOCUMENT: '__substg1.', 1644 | }, 1645 | // example (use fields as needed) 1646 | NAME_MAPPING: { 1647 | // email specific 1648 | '0037': 'subject', 1649 | '0c1a': 'senderName', 1650 | '5d02': 'senderEmail', 1651 | 1000: 'body', 1652 | 1013: 'bodyHTML', 1653 | '007d': 'headers', 1654 | // attachment specific 1655 | 3703: 'extension', 1656 | 3704: 'fileNameShort', 1657 | 3707: 'fileName', 1658 | 3712: 'pidContentId', 1659 | '370e': 'mimeType', 1660 | // recipient specific 1661 | 3001: 'name', 1662 | '39fe': 'email', 1663 | }, 1664 | CLASS_MAPPING: { 1665 | ATTACHMENT_DATA: '3701', 1666 | }, 1667 | TYPE_MAPPING: { 1668 | '001e': 'string', 1669 | '001f': 'unicode', 1670 | '0102': 'binary', 1671 | }, 1672 | DIR_TYPE: { 1673 | INNER_MSG: '000d', 1674 | }, 1675 | }, 1676 | }, 1677 | }; 1678 | 1679 | // unit utils 1680 | function arraysEqual(a, b) { 1681 | if (a === b) return true; 1682 | if (a == null || b == null) return false; 1683 | if (a.length != b.length) return false; 1684 | 1685 | for (var i = 0; i < a.length; i++) { 1686 | if (a[i] !== b[i]) return false; 1687 | } 1688 | return true; 1689 | } 1690 | 1691 | function uInt2int(data) { 1692 | var result = new Array(data.length); 1693 | for (var i = 0; i < data.length; i++) { 1694 | result[i] = (data[i] << 24) >> 24; 1695 | } 1696 | return result; 1697 | } 1698 | 1699 | // MSG Reader implementation 1700 | 1701 | // check MSG file header 1702 | function isMSGFile(ds) { 1703 | ds.seek(0); 1704 | return arraysEqual(CONST.FILE_HEADER, ds.readInt8Array(CONST.FILE_HEADER.length)); 1705 | } 1706 | 1707 | // FAT utils 1708 | function getBlockOffsetAt(msgData, offset) { 1709 | return (offset + 1) * msgData.bigBlockSize; 1710 | } 1711 | 1712 | function getBlockAt(ds, msgData, offset) { 1713 | var startOffset = getBlockOffsetAt(msgData, offset); 1714 | ds.seek(startOffset); 1715 | return ds.readInt32Array(msgData.bigBlockLength); 1716 | } 1717 | 1718 | function getNextBlockInner(ds, msgData, offset, blockOffsetData) { 1719 | var currentBlock = Math.floor(offset / msgData.bigBlockLength); 1720 | var currentBlockIndex = offset % msgData.bigBlockLength; 1721 | 1722 | var startBlockOffset = blockOffsetData[currentBlock]; 1723 | 1724 | return getBlockAt(ds, msgData, startBlockOffset)[currentBlockIndex]; 1725 | } 1726 | 1727 | function getNextBlock(ds, msgData, offset) { 1728 | return getNextBlockInner(ds, msgData, offset, msgData.batData); 1729 | } 1730 | 1731 | function getNextBlockSmall(ds, msgData, offset) { 1732 | return getNextBlockInner(ds, msgData, offset, msgData.sbatData); 1733 | } 1734 | 1735 | // convert binary data to dictionary 1736 | function parseMsgData(ds) { 1737 | var msgData = headerData(ds); 1738 | msgData.batData = batData(ds, msgData); 1739 | msgData.sbatData = sbatData(ds, msgData); 1740 | if (msgData.xbatCount > 0) { 1741 | xbatData(ds, msgData); 1742 | } 1743 | msgData.propertyData = propertyData(ds, msgData); 1744 | msgData.fieldsData = fieldsData(ds, msgData); 1745 | 1746 | return msgData; 1747 | } 1748 | 1749 | // extract header data 1750 | function headerData(ds) { 1751 | var headerData = {}; 1752 | 1753 | // system data 1754 | headerData.bigBlockSize = 1755 | ds.readByte(/*const position*/ 30) == CONST.MSG.L_BIG_BLOCK_MARK 1756 | ? CONST.MSG.L_BIG_BLOCK_SIZE 1757 | : CONST.MSG.S_BIG_BLOCK_SIZE; 1758 | headerData.bigBlockLength = headerData.bigBlockSize / 4; 1759 | headerData.xBlockLength = headerData.bigBlockLength - 1; 1760 | 1761 | // header data 1762 | headerData.batCount = ds.readInt(CONST.MSG.HEADER.BAT_COUNT_OFFSET); 1763 | headerData.propertyStart = ds.readInt(CONST.MSG.HEADER.PROPERTY_START_OFFSET); 1764 | headerData.sbatStart = ds.readInt(CONST.MSG.HEADER.SBAT_START_OFFSET); 1765 | headerData.sbatCount = ds.readInt(CONST.MSG.HEADER.SBAT_COUNT_OFFSET); 1766 | headerData.xbatStart = ds.readInt(CONST.MSG.HEADER.XBAT_START_OFFSET); 1767 | headerData.xbatCount = ds.readInt(CONST.MSG.HEADER.XBAT_COUNT_OFFSET); 1768 | 1769 | return headerData; 1770 | } 1771 | 1772 | function batCountInHeader(msgData) { 1773 | var maxBatsInHeader = (CONST.MSG.S_BIG_BLOCK_SIZE - CONST.MSG.HEADER.BAT_START_OFFSET) / 4; 1774 | return Math.min(msgData.batCount, maxBatsInHeader); 1775 | } 1776 | 1777 | function batData(ds, msgData) { 1778 | var result = new Array(batCountInHeader(msgData)); 1779 | ds.seek(CONST.MSG.HEADER.BAT_START_OFFSET); 1780 | for (var i = 0; i < result.length; i++) { 1781 | result[i] = ds.readInt32(); 1782 | } 1783 | return result; 1784 | } 1785 | 1786 | function sbatData(ds, msgData) { 1787 | var result = []; 1788 | var startIndex = msgData.sbatStart; 1789 | 1790 | for (var i = 0; i < msgData.sbatCount && startIndex != CONST.MSG.END_OF_CHAIN; i++) { 1791 | result.push(startIndex); 1792 | startIndex = getNextBlock(ds, msgData, startIndex); 1793 | } 1794 | return result; 1795 | } 1796 | 1797 | function xbatData(ds, msgData) { 1798 | var batCount = batCountInHeader(msgData); 1799 | var batCountTotal = msgData.batCount; 1800 | var remainingBlocks = batCountTotal - batCount; 1801 | 1802 | var nextBlockAt = msgData.xbatStart; 1803 | for (var i = 0; i < msgData.xbatCount; i++) { 1804 | var xBatBlock = getBlockAt(ds, msgData, nextBlockAt); 1805 | nextBlockAt = xBatBlock[msgData.xBlockLength]; 1806 | 1807 | var blocksToProcess = Math.min(remainingBlocks, msgData.xBlockLength); 1808 | for (var j = 0; j < blocksToProcess; j++) { 1809 | var blockStartAt = xBatBlock[j]; 1810 | if (blockStartAt == CONST.MSG.UNUSED_BLOCK || blockStartAt == CONST.MSG.END_OF_CHAIN) { 1811 | break; 1812 | } 1813 | msgData.batData.push(blockStartAt); 1814 | } 1815 | remainingBlocks -= blocksToProcess; 1816 | } 1817 | } 1818 | 1819 | // extract property data and property hierarchy 1820 | function propertyData(ds, msgData) { 1821 | var props = []; 1822 | 1823 | var currentOffset = msgData.propertyStart; 1824 | 1825 | while (currentOffset != CONST.MSG.END_OF_CHAIN) { 1826 | convertBlockToProperties(ds, msgData, currentOffset, props); 1827 | currentOffset = getNextBlock(ds, msgData, currentOffset); 1828 | } 1829 | createPropertyHierarchy(props, /*property with index 0 (zero) always as root*/ props[0]); 1830 | return props; 1831 | } 1832 | 1833 | function convertName(ds, offset) { 1834 | var nameLength = ds.readShort(offset + CONST.MSG.PROP.NAME_SIZE_OFFSET); 1835 | if (nameLength < 1) { 1836 | return ''; 1837 | } else { 1838 | return ds.readStringAt(offset, nameLength / 2); 1839 | } 1840 | } 1841 | 1842 | function convertProperty(ds, index, offset) { 1843 | return { 1844 | index: index, 1845 | type: ds.readByte(offset + CONST.MSG.PROP.TYPE_OFFSET), 1846 | name: convertName(ds, offset), 1847 | // hierarchy 1848 | previousProperty: ds.readInt(offset + CONST.MSG.PROP.PREVIOUS_PROPERTY_OFFSET), 1849 | nextProperty: ds.readInt(offset + CONST.MSG.PROP.NEXT_PROPERTY_OFFSET), 1850 | childProperty: ds.readInt(offset + CONST.MSG.PROP.CHILD_PROPERTY_OFFSET), 1851 | // data offset 1852 | startBlock: ds.readInt(offset + CONST.MSG.PROP.START_BLOCK_OFFSET), 1853 | sizeBlock: ds.readInt(offset + CONST.MSG.PROP.SIZE_OFFSET), 1854 | }; 1855 | } 1856 | 1857 | function convertBlockToProperties(ds, msgData, propertyBlockOffset, props) { 1858 | var propertyCount = msgData.bigBlockSize / CONST.MSG.PROP.PROPERTY_SIZE; 1859 | var propertyOffset = getBlockOffsetAt(msgData, propertyBlockOffset); 1860 | 1861 | for (var i = 0; i < propertyCount; i++) { 1862 | var propertyType = ds.readByte(propertyOffset + CONST.MSG.PROP.TYPE_OFFSET); 1863 | switch (propertyType) { 1864 | case CONST.MSG.PROP.TYPE_ENUM.ROOT: 1865 | case CONST.MSG.PROP.TYPE_ENUM.DIRECTORY: 1866 | case CONST.MSG.PROP.TYPE_ENUM.DOCUMENT: 1867 | props.push(convertProperty(ds, props.length, propertyOffset)); 1868 | break; 1869 | default: 1870 | /* unknown property types */ 1871 | props.push(null); 1872 | } 1873 | 1874 | propertyOffset += CONST.MSG.PROP.PROPERTY_SIZE; 1875 | } 1876 | } 1877 | 1878 | function createPropertyHierarchy(props, nodeProperty) { 1879 | if (nodeProperty.childProperty == CONST.MSG.PROP.NO_INDEX) { 1880 | return; 1881 | } 1882 | nodeProperty.children = []; 1883 | 1884 | var children = [nodeProperty.childProperty]; 1885 | while (children.length != 0) { 1886 | var currentIndex = children.shift(); 1887 | var current = props[currentIndex]; 1888 | if (current == null) { 1889 | continue; 1890 | } 1891 | nodeProperty.children.push(currentIndex); 1892 | 1893 | if (current.type == CONST.MSG.PROP.TYPE_ENUM.DIRECTORY) { 1894 | createPropertyHierarchy(props, current); 1895 | } 1896 | if (current.previousProperty != CONST.MSG.PROP.NO_INDEX) { 1897 | children.push(current.previousProperty); 1898 | } 1899 | if (current.nextProperty != CONST.MSG.PROP.NO_INDEX) { 1900 | children.push(current.nextProperty); 1901 | } 1902 | } 1903 | } 1904 | 1905 | // extract real fields 1906 | function fieldsData(ds, msgData) { 1907 | var fields = { 1908 | attachments: [], 1909 | recipients: [], 1910 | }; 1911 | fieldsDataDir(ds, msgData, msgData.propertyData[0], fields); 1912 | return fields; 1913 | } 1914 | 1915 | function fieldsDataDir(ds, msgData, dirProperty, fields) { 1916 | if (dirProperty.children && dirProperty.children.length > 0) { 1917 | for (var i = 0; i < dirProperty.children.length; i++) { 1918 | var childProperty = msgData.propertyData[dirProperty.children[i]]; 1919 | 1920 | if (childProperty.type == CONST.MSG.PROP.TYPE_ENUM.DIRECTORY) { 1921 | fieldsDataDirInner(ds, msgData, childProperty, fields); 1922 | } else if ( 1923 | childProperty.type == CONST.MSG.PROP.TYPE_ENUM.DOCUMENT && 1924 | childProperty.name.indexOf(CONST.MSG.FIELD.PREFIX.DOCUMENT) == 0 1925 | ) { 1926 | fieldsDataDocument(ds, msgData, childProperty, fields); 1927 | } 1928 | } 1929 | } 1930 | } 1931 | 1932 | function fieldsDataDirInner(ds, msgData, dirProperty, fields) { 1933 | if (dirProperty.name.indexOf(CONST.MSG.FIELD.PREFIX.ATTACHMENT) == 0) { 1934 | // attachment 1935 | var attachmentField = {}; 1936 | fields.attachments.push(attachmentField); 1937 | fieldsDataDir(ds, msgData, dirProperty, attachmentField); 1938 | } else if (dirProperty.name.indexOf(CONST.MSG.FIELD.PREFIX.RECIPIENT) == 0) { 1939 | // recipient 1940 | var recipientField = {}; 1941 | fields.recipients.push(recipientField); 1942 | fieldsDataDir(ds, msgData, dirProperty, recipientField); 1943 | } else { 1944 | // other dir 1945 | var childFieldType = getFieldType(dirProperty); 1946 | if (childFieldType != CONST.MSG.FIELD.DIR_TYPE.INNER_MSG) { 1947 | fieldsDataDir(ds, msgData, dirProperty, fields); 1948 | } else { 1949 | // MSG as attachment currently isn't supported 1950 | fields.innerMsgContent = true; 1951 | } 1952 | } 1953 | } 1954 | 1955 | function isAddPropertyValue(fieldName, fieldTypeMapped) { 1956 | return fieldName !== 'body' || fieldTypeMapped !== 'binary'; 1957 | } 1958 | 1959 | function fieldsDataDocument(ds, msgData, documentProperty, fields) { 1960 | var value = documentProperty.name.substring(12).toLowerCase(); 1961 | var fieldClass = value.substring(0, 4); 1962 | var fieldType = value.substring(4, 8); 1963 | 1964 | var fieldName = CONST.MSG.FIELD.NAME_MAPPING[fieldClass]; 1965 | var fieldTypeMapped = CONST.MSG.FIELD.TYPE_MAPPING[fieldType]; 1966 | 1967 | if (fieldName) { 1968 | var fieldValue = getFieldValue(ds, msgData, documentProperty, fieldTypeMapped); 1969 | 1970 | if (isAddPropertyValue(fieldName, fieldTypeMapped)) { 1971 | fields[fieldName] = applyValueConverter(fieldName, fieldTypeMapped, fieldValue); 1972 | } 1973 | } 1974 | if (fieldClass == CONST.MSG.FIELD.CLASS_MAPPING.ATTACHMENT_DATA) { 1975 | // attachment specific info 1976 | fields['dataId'] = documentProperty.index; 1977 | fields['contentLength'] = documentProperty.sizeBlock; 1978 | } 1979 | } 1980 | 1981 | // todo: html body test 1982 | function applyValueConverter(fieldName, fieldTypeMapped, fieldValue) { 1983 | if (fieldTypeMapped === 'binary' && fieldName === 'bodyHTML') { 1984 | return convertUint8ArrayToString(fieldValue); 1985 | } 1986 | return fieldValue; 1987 | } 1988 | 1989 | function getFieldType(fieldProperty) { 1990 | var value = fieldProperty.name.substring(12).toLowerCase(); 1991 | return value.substring(4, 8); 1992 | } 1993 | 1994 | // extractor structure to manage bat/sbat block types and different data types 1995 | var extractorFieldValue = { 1996 | sbat: { 1997 | extractor: function extractDataViaSbat(ds, msgData, fieldProperty, dataTypeExtractor) { 1998 | var chain = getChainByBlockSmall(ds, msgData, fieldProperty); 1999 | if (chain.length == 1) { 2000 | return readDataByBlockSmall( 2001 | ds, 2002 | msgData, 2003 | fieldProperty.startBlock, 2004 | fieldProperty.sizeBlock, 2005 | dataTypeExtractor 2006 | ); 2007 | } else if (chain.length > 1) { 2008 | return readChainDataByBlockSmall(ds, msgData, fieldProperty, chain, dataTypeExtractor); 2009 | } 2010 | return null; 2011 | }, 2012 | dataType: { 2013 | string: function extractBatString(ds, msgData, blockStartOffset, bigBlockOffset, blockSize) { 2014 | ds.seek(blockStartOffset + bigBlockOffset); 2015 | return ds.readString(blockSize); 2016 | }, 2017 | unicode: function extractBatUnicode(ds, msgData, blockStartOffset, bigBlockOffset, blockSize) { 2018 | ds.seek(blockStartOffset + bigBlockOffset); 2019 | return ds.readUCS2String(blockSize / 2); 2020 | }, 2021 | binary: function extractBatBinary(ds, msgData, blockStartOffset, bigBlockOffset, blockSize) { 2022 | ds.seek(blockStartOffset + bigBlockOffset); 2023 | return ds.readUint8Array(blockSize); 2024 | }, 2025 | }, 2026 | }, 2027 | bat: { 2028 | extractor: function extractDataViaBat(ds, msgData, fieldProperty, dataTypeExtractor) { 2029 | var offset = getBlockOffsetAt(msgData, fieldProperty.startBlock); 2030 | ds.seek(offset); 2031 | return dataTypeExtractor(ds, fieldProperty); 2032 | }, 2033 | dataType: { 2034 | string: function extractSbatString(ds, fieldProperty) { 2035 | return ds.readString(fieldProperty.sizeBlock); 2036 | }, 2037 | unicode: function extractSbatUnicode(ds, fieldProperty) { 2038 | return ds.readUCS2String(fieldProperty.sizeBlock / 2); 2039 | }, 2040 | binary: function extractSbatBinary(ds, fieldProperty) { 2041 | return ds.readUint8Array(fieldProperty.sizeBlock); 2042 | }, 2043 | }, 2044 | }, 2045 | }; 2046 | 2047 | function readDataByBlockSmall(ds, msgData, startBlock, blockSize, dataTypeExtractor) { 2048 | var byteOffset = startBlock * CONST.MSG.SMALL_BLOCK_SIZE; 2049 | var bigBlockNumber = Math.floor(byteOffset / msgData.bigBlockSize); 2050 | var bigBlockOffset = byteOffset % msgData.bigBlockSize; 2051 | 2052 | var rootProp = msgData.propertyData[0]; 2053 | 2054 | var nextBlock = rootProp.startBlock; 2055 | for (var i = 0; i < bigBlockNumber; i++) { 2056 | nextBlock = getNextBlock(ds, msgData, nextBlock); 2057 | } 2058 | var blockStartOffset = getBlockOffsetAt(msgData, nextBlock); 2059 | 2060 | return dataTypeExtractor(ds, msgData, blockStartOffset, bigBlockOffset, blockSize); 2061 | } 2062 | 2063 | function readChainDataByBlockSmall(ds, msgData, fieldProperty, chain, dataTypeExtractor) { 2064 | var resultData = new Int8Array(fieldProperty.sizeBlock); 2065 | 2066 | for (var i = 0, idx = 0; i < chain.length; i++) { 2067 | var data = readDataByBlockSmall( 2068 | ds, 2069 | msgData, 2070 | chain[i], 2071 | CONST.MSG.SMALL_BLOCK_SIZE, 2072 | extractorFieldValue.sbat.dataType.binary 2073 | ); 2074 | for (var j = 0; j < data.length; j++) { 2075 | resultData[idx++] = data[j]; 2076 | } 2077 | } 2078 | var localDs = new DataStream(resultData, 0, DataStream.LITTLE_ENDIAN); 2079 | return dataTypeExtractor(localDs, msgData, 0, 0, fieldProperty.sizeBlock); 2080 | } 2081 | 2082 | function getChainByBlockSmall(ds, msgData, fieldProperty) { 2083 | var blockChain = []; 2084 | var nextBlockSmall = fieldProperty.startBlock; 2085 | while (nextBlockSmall != CONST.MSG.END_OF_CHAIN) { 2086 | blockChain.push(nextBlockSmall); 2087 | nextBlockSmall = getNextBlockSmall(ds, msgData, nextBlockSmall); 2088 | } 2089 | return blockChain; 2090 | } 2091 | 2092 | function getFieldValue(ds, msgData, fieldProperty, typeMapped) { 2093 | var value = null; 2094 | 2095 | var valueExtractor = 2096 | fieldProperty.sizeBlock < CONST.MSG.BIG_BLOCK_MIN_DOC_SIZE 2097 | ? extractorFieldValue.sbat 2098 | : extractorFieldValue.bat; 2099 | var dataTypeExtractor = valueExtractor.dataType[typeMapped]; 2100 | 2101 | if (dataTypeExtractor) { 2102 | value = valueExtractor.extractor(ds, msgData, fieldProperty, dataTypeExtractor); 2103 | } 2104 | return value; 2105 | } 2106 | 2107 | function convertUint8ArrayToString(uint8ArraValue) { 2108 | return new TextDecoder('utf-8').decode(uint8ArraValue); 2109 | } 2110 | 2111 | // MSG Reader 2112 | var MSGReader = function (arrayBuffer) { 2113 | this.ds = new DataStream(arrayBuffer, 0, DataStream.LITTLE_ENDIAN); 2114 | }; 2115 | 2116 | MSGReader.prototype = { 2117 | /** 2118 | Converts bytes to fields information 2119 | 2120 | @return {Object} The fields data for MSG file 2121 | */ 2122 | getFileData: function () { 2123 | if (!isMSGFile(this.ds)) { 2124 | return { error: 'Unsupported file type!' }; 2125 | } 2126 | if (this.fileData == null) { 2127 | this.fileData = parseMsgData(this.ds); 2128 | } 2129 | return this.fileData.fieldsData; 2130 | }, 2131 | /** 2132 | Reads an attachment content by key/ID 2133 | 2134 | @return {Object} The attachment for specific attachment key 2135 | */ 2136 | getAttachment: function (attach) { 2137 | var attachData = typeof attach === 'number' ? this.fileData.fieldsData.attachments[attach] : attach; 2138 | var fieldProperty = this.fileData.propertyData[attachData.dataId]; 2139 | var fieldTypeMapped = CONST.MSG.FIELD.TYPE_MAPPING[getFieldType(fieldProperty)]; 2140 | var fieldData = getFieldValue(this.ds, this.fileData, fieldProperty, fieldTypeMapped); 2141 | 2142 | return { fileName: attachData.fileName, content: fieldData }; 2143 | }, 2144 | }; 2145 | 2146 | module.exports = { 2147 | MSGReader, 2148 | DataStream, 2149 | }; 2150 | --------------------------------------------------------------------------------