+28
-0
@@ -8,2 +8,30 @@ # Changelog | ||
| ## [2.1.2] - 2025-10-17 | ||
| ### 🎨 Enhanced User Interface | ||
| #### Added | ||
| - **Colorful Help Display**: Completely redesigned help output with vibrant colors | ||
| - Cyan bordered header with version display (🚀 OST2GO v2.1.2) | ||
| - Colorful credits box showing author, website, and GitHub links | ||
| - Yellow "Usage" section, green "Options" and "Commands" headers | ||
| - Cyan command and option flags for better readability | ||
| - White descriptions with proper formatting | ||
| - **Website Integration**: Added official website link (https://ost2go.kief.fi) to help display | ||
| - Blue underlined links for website and GitHub | ||
| - Displayed prominently with emoji indicators (🌐 and 📂) | ||
| - **Credits Everywhere**: Credits box now appears on all help displays | ||
| - Shows on `--help`, `-h`, and when no arguments provided | ||
| - Enhanced with emojis: 👤 Author, 🌐 Website, 📂 GitHub, 📦 Project | ||
| - Colorized with cyan borders, green author name, and blue links | ||
| #### Changed | ||
| - Upgraded credits display from gray to colorful cyan-bordered box | ||
| - Improved help output formatting with better color coordination | ||
| - Enhanced branding visibility throughout CLI interface | ||
| #### Fixed | ||
| - Removed duplicate help message that appeared when running without arguments | ||
| - Fixed redundant help display in Commander.js setup | ||
| ## [2.1.0] - 2025-10-17 | ||
@@ -10,0 +38,0 @@ |
+1
-1
| { | ||
| "name": "ost2go", | ||
| "version": "2.1.1", | ||
| "version": "2.1.2", | ||
| "description": "A comprehensive Node.js application to convert, extract, and manage Microsoft Outlook OST files with UTF-8 support", | ||
@@ -5,0 +5,0 @@ "main": "src/index.js", |
+5
-0
@@ -17,2 +17,6 @@ # 🚀 OST2GO -  | ||
| [](https://github.com/SkyLostTR/OST2GO/actions) | ||
| [](https://github.com/SkyLostTR/OST2GO/network/members) | ||
| [](https://github.com/SkyLostTR/OST2GO/graphs/contributors) | ||
| [](https://github.com/SkyLostTR/OST2GO/commits/main) | ||
| [](https://github.com/SkyLostTR/OST2GO) | ||
@@ -430,1 +434,2 @@ **Created by [SkyLostTR](https://github.com/SkyLostTR) (@Keeftraum)** | ||
+355
-38
@@ -7,3 +7,3 @@ /** | ||
| * @author SkyLostTR (@Keeftraum) | ||
| * @version 2.0.0 | ||
| * @version require('../package.json').version; | ||
| * @license SEE LICENSE IN LICENSE | ||
@@ -22,9 +22,170 @@ * @repository https://github.com/SkyLostTR/OST2GO | ||
| const { PSTFile } = require('pst-extractor'); | ||
| const packageInfo = require('../package.json'); | ||
| const program = new Command(); | ||
| /** | ||
| * Helper function to format ETA in seconds and minutes | ||
| * @param {number} eta - ETA in seconds | ||
| * @returns {string} Formatted ETA string | ||
| */ | ||
| function formatETA(eta) { | ||
| if (isNaN(eta) || eta === Infinity || eta < 0 || eta === 0) { | ||
| return '--'; | ||
| } | ||
| const totalSeconds = Math.round(eta); | ||
| const minutes = Math.floor(totalSeconds / 60); | ||
| const seconds = totalSeconds % 60; | ||
| if (minutes > 0) { | ||
| return `${minutes}m ${seconds}s`; | ||
| } | ||
| return `${seconds}s`; | ||
| } | ||
| /** | ||
| * Create a custom ProgressBar with formatted ETA in minutes and seconds | ||
| */ | ||
| function createProgressBar(format, options) { | ||
| // We'll manually manage the progress display | ||
| let bar = null; | ||
| let startTime = Date.now(); | ||
| let lastRenderTime = 0; | ||
| const renderThrottle = options.renderThrottle || 16; | ||
| // Create a wrapper object that mimics ProgressBar interface | ||
| const wrapper = { | ||
| curr: 0, | ||
| total: options.total, | ||
| startTime: startTime, | ||
| tick: function(len = 1, tokens = {}) { | ||
| this.curr += len; | ||
| if (this.curr > this.total) this.curr = this.total; | ||
| // Throttle rendering | ||
| const now = Date.now(); | ||
| if (now - lastRenderTime < renderThrottle && this.curr < this.total) { | ||
| return; | ||
| } | ||
| lastRenderTime = now; | ||
| // Calculate ETA | ||
| const elapsed = (now - startTime) / 1000; | ||
| const rate = this.curr / elapsed; | ||
| const remaining = this.total - this.curr; | ||
| const eta = remaining > 0 && rate > 0 ? remaining / rate : 0; | ||
| // Build the progress bar manually | ||
| const percent = Math.floor((this.curr / this.total) * 100); | ||
| const ratio = this.curr / this.total; | ||
| const width = options.width || 40; | ||
| const complete = Math.floor(width * ratio); | ||
| const incomplete = width - complete; | ||
| const completeChar = options.complete || '█'; | ||
| const incompleteChar = options.incomplete || '░'; | ||
| let bar = '['; | ||
| for (let i = 0; i < complete; i++) bar += completeChar; | ||
| for (let i = 0; i < incomplete; i++) bar += incompleteChar; | ||
| bar += ']'; | ||
| // Build the display line | ||
| let line = format | ||
| .replace(':bar', bar) | ||
| .replace(':current', this.curr.toString()) | ||
| .replace(':total', this.total.toString()) | ||
| .replace(':percent', percent + '%'); | ||
| // Add ETA | ||
| line += chalk.magenta(' ETA: ' + formatETA(eta)); | ||
| // Clear line and write | ||
| process.stdout.clearLine(0); | ||
| process.stdout.cursorTo(0); | ||
| process.stdout.write(line); | ||
| }, | ||
| update: function(ratio, tokens = {}) { | ||
| this.curr = Math.floor(ratio * this.total); | ||
| this.tick(0, tokens); | ||
| }, | ||
| terminate: function() { | ||
| process.stdout.write('\n'); | ||
| } | ||
| }; | ||
| return wrapper; | ||
| } | ||
| /** | ||
| * Display credits header for commands | ||
| */ | ||
| function showCredits() { | ||
| console.log(chalk.cyan('┌────────────────────────────────────────────────────────────┐')); | ||
| console.log(chalk.cyan('│ ') + chalk.bold.white('👤 Author: ') + chalk.green('SkyLostTR') + chalk.gray(' (@Keeftraum)') + chalk.cyan(' │')); | ||
| console.log(chalk.cyan('│ ') + chalk.bold.white('🌐 Website: ') + chalk.blue.underline('https://ost2go.kief.fi') + chalk.cyan(' │')); | ||
| console.log(chalk.cyan('│ ') + chalk.bold.white('📂 GitHub: ') + chalk.blue.underline('https://github.com/SkyLostTR/OST2GO') + chalk.cyan(' │')); | ||
| console.log(chalk.cyan('│ ') + chalk.bold.white('📦 Project: ') + chalk.magenta('OST2GO - OST/PST Management Toolkit') + chalk.cyan(' │')); | ||
| console.log(chalk.cyan('└────────────────────────────────────────────────────────────┘')); | ||
| } | ||
| // Get package.json data | ||
| // Override help display to show credits and colors | ||
| const originalOutputHelp = program.outputHelp.bind(program); | ||
| program.outputHelp = function(cb) { | ||
| // Display colorful header | ||
| console.log(chalk.bold.cyan('\n╔══════════════════════════════════════════════════════════════╗')); | ||
| console.log(chalk.bold.cyan('║') + chalk.bold.white(' 🚀 OST2GO v' + packageInfo.version + ' ') + chalk.bold.cyan('║')); | ||
| console.log(chalk.bold.cyan('╚══════════════════════════════════════════════════════════════╝\n')); | ||
| // Show credits | ||
| showCredits(); | ||
| console.log(''); | ||
| // Call original help but with color processing | ||
| return originalOutputHelp(function(str) { | ||
| // Remove the plain description line that comes after Usage | ||
| str = str.replace(/\n\n[A-Z].*OST files with UTF-8 support\n\n/g, '\n\n'); | ||
| // Colorize different parts | ||
| str = str.replace(/(Usage:)(.+)/g, chalk.bold.yellow('$1') + chalk.white('$2')); | ||
| str = str.replace(/(Options:)/g, chalk.bold.green('$1')); | ||
| str = str.replace(/(Commands:)/g, chalk.bold.green('$1')); | ||
| // Colorize options (flags) | ||
| str = str.replace(/\s{2}(-[V-Zh-z]),\s(--[\w-]+)/g, ' ' + chalk.cyan('$1') + ', ' + chalk.cyan('$2')); | ||
| // Colorize command names at start of line | ||
| str = str.replace(/\n\s{2}(convert|info|extract|validate|help)(\s)/g, '\n ' + chalk.cyan('$1') + '$2'); | ||
| // Colorize descriptions | ||
| str = str.replace(/(output the version number)/g, chalk.white('$1')); | ||
| str = str.replace(/(display help for command)/g, chalk.white('$1')); | ||
| str = str.replace(/(Convert OST file to PST format)/g, chalk.white('$1')); | ||
| str = str.replace(/(Display information about an OST file)/g, chalk.white('$1')); | ||
| str = str.replace(/(Extract emails from OST\/PST file to EML, MBOX, and JSON formats)/g, chalk.white('$1')); | ||
| str = str.replace(/(Validate PST file integrity and contents using pst-extractor library)/g, chalk.white('$1')); | ||
| // Add colorful description after Usage | ||
| const descLine = chalk.bold.cyan('OST2GO') + chalk.gray(' by ') + chalk.green('SkyLostTR') + chalk.gray(' (@Keeftraum)'); | ||
| const desc = chalk.gray('📦 ') + chalk.white(packageInfo.description); | ||
| const website = chalk.gray('🌐 Website: ') + chalk.blue.underline('https://ost2go.kief.fi'); | ||
| const github = chalk.gray('📂 GitHub: ') + chalk.blue.underline('https://github.com/SkyLostTR/OST2GO'); | ||
| // Insert description after Usage line | ||
| str = str.replace(/(Usage:.+\n)\n/, `$1\n${descLine}\n${desc}\n${website}\n${github}\n\n`); | ||
| return str; | ||
| }); | ||
| }; | ||
| program | ||
| .name('ost2go') | ||
| .description('OST2GO by SkyLostTR (@Keeftraum) - Complete OST/PST management toolkit') | ||
| .version('2.0.0'); | ||
| .description(packageInfo.description) | ||
| .version(packageInfo.version); | ||
@@ -48,2 +209,3 @@ program | ||
| console.log(chalk.bold.cyan('╚══════════════════════════════════════════════════════════════╝')); | ||
| showCredits(); | ||
@@ -59,4 +221,4 @@ // Check if user wants real conversion | ||
| // Setup progress bar for real conversion | ||
| const progressBar = new ProgressBar( | ||
| chalk.cyan('🔄 Converting: ') + chalk.white('[:bar] ') + chalk.yellow(':current/:total ') + chalk.gray('(:percent)') + chalk.magenta(' ETA: :etas'), | ||
| const progressBar = createProgressBar( | ||
| chalk.cyan('🔄 Converting: ') + chalk.white('[:bar] ') + chalk.yellow(':current/:total ') + chalk.gray('(:percent)'), | ||
| { | ||
@@ -174,4 +336,4 @@ total: 4, // 4 main steps | ||
| // Setup progress bar for legacy conversion | ||
| const progressBar = new ProgressBar( | ||
| chalk.cyan('🔄 Converting: ') + chalk.white('[:bar] ') + chalk.gray('(:percent)') + chalk.magenta(' ETA: :etas'), | ||
| const progressBar = createProgressBar( | ||
| chalk.cyan('🔄 Converting: ') + chalk.white('[:bar] ') + chalk.gray('(:percent)'), | ||
| { | ||
@@ -245,2 +407,3 @@ total: 100, // Simulate progress for legacy converter | ||
| console.log(chalk.bold.cyan('╚══════════════════════════════════════════════════════════════╝')); | ||
| showCredits(); | ||
@@ -255,4 +418,4 @@ if (!await fs.pathExists(options.input)) { | ||
| // Setup progress bar for analysis | ||
| const progressBar = new ProgressBar( | ||
| chalk.cyan('📋 Analyzing: ') + chalk.white('[:bar] ') + chalk.gray('(:percent)') + chalk.magenta(' ETA: :etas'), | ||
| const progressBar = createProgressBar( | ||
| chalk.cyan('📋 Analyzing: ') + chalk.white('[:bar] ') + chalk.gray('(:percent)'), | ||
| { | ||
@@ -341,2 +504,3 @@ total: 100, | ||
| console.log(chalk.bold.cyan('╚══════════════════════════════════════════════════════════════╝')); | ||
| showCredits(); | ||
@@ -376,4 +540,4 @@ const inputPath = path.resolve(options.input); | ||
| // Setup progress bar | ||
| const progressBar = new ProgressBar( | ||
| chalk.cyan('📧 Extracting: ') + chalk.white('[:bar] ') + chalk.yellow(':current/:total ') + chalk.gray('(:percent)') + chalk.magenta(' ETA: :etas'), | ||
| const progressBar = createProgressBar( | ||
| chalk.cyan('📧 Extracting: ') + chalk.white('[:bar] ') + chalk.yellow(':current/:total ') + chalk.gray('(:percent)'), | ||
| { | ||
@@ -452,25 +616,74 @@ total: maxEmails, | ||
| let attachData = null; | ||
| let extractionMethod = 'none'; | ||
| // Method 1: Try fileInputStream with improved block reading | ||
| try { | ||
| const stream = attach.fileInputStream; | ||
| if (stream) { | ||
| const chunks = []; | ||
| let totalSize = 0; | ||
| const bufferSize = 8176; | ||
| const buffer = Buffer.alloc(bufferSize); | ||
| try { | ||
| let bytesRead = stream.read(buffer); | ||
| while (bytesRead > 0) { | ||
| chunks.push(Buffer.from(buffer.slice(0, bytesRead))); | ||
| totalSize += bytesRead; | ||
| bytesRead = stream.read(buffer); | ||
| // First check if allData is available (already decompressed) | ||
| if (stream.allData && stream.allData.length > 0) { | ||
| attachData = stream.allData; | ||
| extractionMethod = 'stream-alldata'; | ||
| } | ||
| // Check if we can read blocks directly (for problematic compressions) | ||
| else if (stream.indexItems && stream.indexItems.length > 0) { | ||
| const chunks = []; | ||
| let totalSize = 0; | ||
| const zlib = require('zlib'); | ||
| for (const item of stream.indexItems) { | ||
| try { | ||
| const blockBuffer = Buffer.alloc(item.size); | ||
| attach.pstFile.seek(item.fileOffset); | ||
| attach.pstFile.readCompletely(blockBuffer); | ||
| // Check if zlib compressed | ||
| if (blockBuffer.length > 2 && blockBuffer[0] === 0x78 && blockBuffer[1] === 0x9c) { | ||
| try { | ||
| const decompressed = zlib.unzipSync(blockBuffer); | ||
| chunks.push(decompressed); | ||
| totalSize += decompressed.length; | ||
| } catch (err) { | ||
| // Decompression failed, use raw | ||
| chunks.push(blockBuffer); | ||
| totalSize += blockBuffer.length; | ||
| } | ||
| } else { | ||
| chunks.push(blockBuffer); | ||
| totalSize += blockBuffer.length; | ||
| } | ||
| } catch (blockErr) { | ||
| // Skip bad blocks | ||
| } | ||
| } | ||
| if (totalSize > 0) { | ||
| if (chunks.length > 0) { | ||
| attachData = Buffer.concat(chunks, totalSize); | ||
| extractionMethod = 'stream-blocks'; | ||
| } | ||
| } catch (zlibErr) { | ||
| skippedAttachments++; | ||
| if (options.verbose) { | ||
| console.log(chalk.gray(` ⚠️ Skipping attachment ${i} (compression error)`)); | ||
| } | ||
| // Fall back to normal stream reading | ||
| else { | ||
| const chunks = []; | ||
| let totalSize = 0; | ||
| const bufferSize = 8176; | ||
| const buffer = Buffer.alloc(bufferSize); | ||
| try { | ||
| let bytesRead = stream.read(buffer); | ||
| while (bytesRead > 0) { | ||
| chunks.push(Buffer.from(buffer.slice(0, bytesRead))); | ||
| totalSize += bytesRead; | ||
| bytesRead = stream.read(buffer); | ||
| } | ||
| if (totalSize > 0) { | ||
| attachData = Buffer.concat(chunks, totalSize); | ||
| extractionMethod = 'stream'; | ||
| } | ||
| } catch (zlibErr) { | ||
| // Compression error - try alternative method | ||
| if (options.verbose) { | ||
| console.log(chalk.yellow(` ⚠️ Compression error on ${attach.longFilename || attach.filename || `attachment${i}`}, trying alternative method...`)); | ||
| } | ||
| } | ||
@@ -480,5 +693,111 @@ } | ||
| } catch (streamErr) { | ||
| // Stream error - try alternative method | ||
| if (options.verbose) { | ||
| console.log(chalk.yellow(` ⚠️ Stream error on ${attach.longFilename || attach.filename || `attachment${i}`}, trying alternative method...`)); | ||
| } | ||
| } | ||
| // Method 2: Try direct property table access before decompression | ||
| if (!attachData) { | ||
| try { | ||
| // Try to get PidTagAttachDataBinary (0x3701) - the raw property | ||
| const dataProperty = attach.pstTableItems?.get(0x3701); | ||
| if (dataProperty) { | ||
| // If it's an external reference, we need to get the descriptor item | ||
| if (dataProperty.isExternalValueReference) { | ||
| const descriptorItem = attach.localDescriptorItems?.get(dataProperty.entryValueReference); | ||
| if (descriptorItem) { | ||
| // Try to read the raw data from the PST file | ||
| try { | ||
| // Get the offset item and read raw data | ||
| const Long = require('long'); | ||
| const offsetId = Long.isLong(descriptorItem.offsetIndexIdentifier) | ||
| ? descriptorItem.offsetIndexIdentifier | ||
| : Long.fromNumber(descriptorItem.offsetIndexIdentifier); | ||
| const offsetItem = attach.pstFile.getOffsetIndexNode(offsetId); | ||
| if (offsetItem) { | ||
| const rawBuffer = Buffer.alloc(offsetItem.size); | ||
| attach.pstFile.seek(offsetItem.fileOffset); | ||
| attach.pstFile.readCompletely(rawBuffer); | ||
| // Try to decompress manually with better error handling | ||
| if (rawBuffer.length > 2 && rawBuffer[0] === 0x78 && rawBuffer[1] === 0x9c) { | ||
| // This is zlib compressed | ||
| try { | ||
| const zlib = require('zlib'); | ||
| attachData = zlib.unzipSync(rawBuffer); | ||
| extractionMethod = 'manual-zlib'; | ||
| if (options.verbose) { | ||
| console.log(chalk.green(` ✅ Extracted ${attach.longFilename || attach.filename || `attachment${i}`} using manual zlib decompression (${attachData.length} bytes)`)); | ||
| } | ||
| } catch (zlibManualErr) { | ||
| // Try inflateSync as alternative | ||
| try { | ||
| const zlib = require('zlib'); | ||
| attachData = zlib.inflateSync(rawBuffer); | ||
| extractionMethod = 'manual-inflate'; | ||
| if (options.verbose) { | ||
| console.log(chalk.green(` ✅ Extracted ${attach.longFilename || attach.filename || `attachment${i}`} using manual inflate (${attachData.length} bytes)`)); | ||
| } | ||
| } catch (inflateErr) { | ||
| if (options.verbose) { | ||
| console.log(chalk.yellow(` ⚠️ Manual decompression failed, using raw compressed data (${rawBuffer.length} bytes)`)); | ||
| } | ||
| // Use the raw compressed data as last resort | ||
| attachData = rawBuffer; | ||
| extractionMethod = 'raw-compressed'; | ||
| } | ||
| } | ||
| } else { | ||
| // Not compressed, use as-is | ||
| attachData = rawBuffer; | ||
| extractionMethod = 'raw-uncompressed'; | ||
| if (options.verbose && attachData.length > 0) { | ||
| console.log(chalk.green(` ✅ Extracted ${attach.longFilename || attach.filename || `attachment${i}`} using raw method (${attachData.length} bytes)`)); | ||
| } | ||
| } | ||
| } | ||
| } catch (rawErr) { | ||
| if (options.verbose) { | ||
| console.log(chalk.gray(` ⚠️ Raw data extraction error: ${rawErr.message}`)); | ||
| } | ||
| } | ||
| } | ||
| } else if (dataProperty.data) { | ||
| // Internal value reference | ||
| attachData = dataProperty.data; | ||
| extractionMethod = 'property-internal'; | ||
| if (options.verbose) { | ||
| console.log(chalk.green(` ✅ Extracted ${attach.longFilename || attach.filename || `attachment${i}`} using internal property`)); | ||
| } | ||
| } | ||
| } | ||
| } catch (propErr) { | ||
| if (options.verbose) { | ||
| console.log(chalk.gray(` ⚠️ Property method error: ${propErr.message}`)); | ||
| } | ||
| } | ||
| } | ||
| // Method 3: Try attachDataBinary property | ||
| if (!attachData && attach.attachDataBinary) { | ||
| try { | ||
| attachData = attach.attachDataBinary; | ||
| extractionMethod = 'binary'; | ||
| if (options.verbose) { | ||
| console.log(chalk.green(` ✅ Extracted ${attach.longFilename || attach.filename || `attachment${i}`} using binary method`)); | ||
| } | ||
| } catch (binaryErr) { | ||
| if (options.verbose) { | ||
| console.log(chalk.gray(` ⚠️ Binary extraction failed`)); | ||
| } | ||
| } | ||
| } | ||
| // If all methods failed | ||
| if (!attachData) { | ||
| skippedAttachments++; | ||
| if (options.verbose) { | ||
| console.log(chalk.gray(` ⚠️ Attachment ${i} stream error`)); | ||
| console.log(chalk.red(` ❌ Failed to extract ${attach.longFilename || attach.filename || `attachment${i}`} - all methods failed`)); | ||
| } | ||
@@ -491,3 +810,4 @@ } | ||
| size: attach.attachSize || 0, | ||
| data: attachData | ||
| data: attachData, | ||
| extractionMethod: extractionMethod | ||
| }); | ||
@@ -498,3 +818,3 @@ } | ||
| if (options.verbose) { | ||
| console.log(chalk.gray(` ⚠️ Attachment ${i} error`)); | ||
| console.log(chalk.red(` ❌ Attachment ${i} error: ${e.message}`)); | ||
| } | ||
@@ -671,2 +991,3 @@ } | ||
| console.log(chalk.bold.cyan('╚══════════════════════════════════════════════════════════════╝')); | ||
| showCredits(); | ||
@@ -688,4 +1009,4 @@ const inputPath = path.resolve(options.input); | ||
| // Setup progress bar for validation | ||
| const progressBar = new ProgressBar( | ||
| chalk.cyan('🔍 Validating: ') + chalk.white('[:bar] ') + chalk.gray('(:percent)') + chalk.magenta(' ETA: :etas'), | ||
| const progressBar = createProgressBar( | ||
| chalk.cyan('🔍 Validating: ') + chalk.white('[:bar] ') + chalk.gray('(:percent)'), | ||
| { | ||
@@ -769,7 +1090,3 @@ total: 100, | ||
| // Show help when no command is provided | ||
| if (!process.argv.slice(2).length) { | ||
| program.outputHelp(); | ||
| } | ||
| // Commander.js automatically shows help when no command is provided | ||
| program.parse(process.argv); |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 2 instances in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
236421
8.42%3189
9.63%434
1.17%25
25%