├── .vscode └── extensions.json ├── .gitignore ├── tests ├── tsconfig.json ├── creating.test.ts ├── utils │ └── checkPdu.ts ├── 7bit-encoding-decoding.test.ts ├── appending.test.ts └── parsing.test.ts ├── .npmignore ├── .prettierignore ├── prettier.config.mjs ├── .editorconfig ├── src ├── index.ts ├── utils │ ├── index.ts │ ├── Type │ │ ├── ReportType.ts │ │ ├── DeliverType.ts │ │ ├── SubmitType.ts │ │ └── PDUType.ts │ ├── Data │ │ ├── Part.ts │ │ ├── Header.ts │ │ └── Data.ts │ ├── SCTS.ts │ ├── SCA │ │ ├── SCAType.ts │ │ └── SCA.ts │ ├── VP.ts │ ├── PID.ts │ ├── PDU.ts │ ├── DCS.ts │ └── Helper.ts ├── parse │ ├── parseUtils │ │ ├── parsePID.ts │ │ ├── parseType.ts │ │ ├── parseVP.ts │ │ ├── parseDCS.ts │ │ ├── parseSCA.ts │ │ ├── parseSCTS.ts │ │ └── parseData.ts │ └── index.ts ├── Deliver.ts ├── Report.ts └── Submit.ts ├── tsconfig.json ├── eslint.config.mjs ├── LICENSE ├── .github └── workflows │ ├── ReleaseNPM.yml │ ├── PublishDocs.yml │ └── CheckCode.yml ├── tsup.config.ts ├── package.json └── README.md /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["editorconfig.editorconfig", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | .pnpm-store 3 | node_modules 4 | 5 | # Logs 6 | *.log* 7 | 8 | # Other 9 | docs 10 | dist 11 | tests-dist 12 | -------------------------------------------------------------------------------- /tests/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "include": ["**/*.ts"], 4 | "compilerOptions": { 5 | "rootDir": ".." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignore everything by default 2 | * 3 | 4 | # Include specific directories and files 5 | !dist/**/* 6 | !LICENSE 7 | !package.json 8 | !README.md 9 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Node modules 2 | .pnpm-store 3 | node_modules 4 | pnpm-lock.yaml 5 | 6 | # Logs 7 | *.log* 8 | 9 | # Other 10 | docs 11 | dist 12 | tests-dist 13 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import("prettier").Options} 3 | */ 4 | export default { 5 | semi: true, 6 | trailingComma: 'none', 7 | singleQuote: true, 8 | printWidth: 140 9 | }; 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{yml,md}] 12 | indent_style = space 13 | indent_size = 2 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // export parse 2 | export { parse } from './parse/index'; 3 | 4 | // export the classes 5 | export { Deliver, type DeliverOptions } from './Deliver'; 6 | export { Report, type ReportOptions } from './Report'; 7 | export { Submit, type SubmitOptions } from './Submit'; 8 | 9 | // export the utils 10 | export * as utils from './utils'; 11 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Data/Data'; 2 | export * from './Data/Header'; 3 | export * from './Data/Part'; 4 | export * from './DCS'; 5 | export * from './Helper'; 6 | export * from './PDU'; 7 | export * from './PID'; 8 | export * from './SCA/SCA'; 9 | export * from './SCA/SCAType'; 10 | export * from './SCTS'; 11 | export * from './Type/DeliverType'; 12 | export * from './Type/PDUType'; 13 | export * from './Type/ReportType'; 14 | export * from './Type/SubmitType'; 15 | export * from './VP'; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "include": ["src/**/*.ts"], 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "esModuleInterop": true, 8 | "exactOptionalPropertyTypes": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "isolatedModules": true, 11 | "module": "ESNext", 12 | "moduleResolution": "bundler", 13 | "noFallthroughCasesInSwitch": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "noUnusedParameters": true, 19 | "outDir": "dist", 20 | "preserveWatchOutput": true, 21 | "rootDir": "src", 22 | "skipLibCheck": true, 23 | "sourceMap": true, 24 | "strict": true, 25 | "target": "ES2022" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/parse/parseUtils/parsePID.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from '../../utils/Helper'; 2 | import { PID } from '../../utils/PID'; 3 | import type { GetSubstr } from '../index'; 4 | 5 | /** 6 | * Parses Protocol Identifier (PID) from a PDU string. 7 | * 8 | * This function extracts Protocol Identifier information from the provided PDU string 9 | * and constructs a PID object to represent it. 10 | * 11 | * @param getPduSubstr A function to extract substrings from the PDU string 12 | * @returns An instance of PID containing parsed information 13 | */ 14 | export default function parsePID(getPduSubstr: GetSubstr) { 15 | const byte = Helper.getByteFromHex(getPduSubstr(2)); 16 | const pid = new PID(); 17 | 18 | pid.setPid(byte >> 6); 19 | pid.setIndicates(byte >> 5); 20 | pid.setType(byte); 21 | 22 | return pid; 23 | } 24 | -------------------------------------------------------------------------------- /src/utils/Type/ReportType.ts: -------------------------------------------------------------------------------- 1 | import { PDUType } from './PDUType'; 2 | 3 | /** 4 | * Represents the PDU type for an SMS-REPORT message in GSM SMS messaging. 5 | */ 6 | export class ReportType extends PDUType { 7 | readonly messageTypeIndicator = PDUType.SMS_REPORT; 8 | 9 | /** 10 | * Constructs a ReportType instance. 11 | * @param params Parameters for configuring the ReportType instance 12 | */ 13 | constructor(params: ReportParams = {}) { 14 | super({ 15 | replyPath: params.replyPath ? 1 & params.replyPath : 0, 16 | userDataHeader: params.userDataHeader ? 1 & params.userDataHeader : 0, 17 | statusReportRequest: params.statusReportRequest ? 1 & params.statusReportRequest : 0, 18 | rejectDuplicates: params.mms ? 1 & params.mms : 0, 19 | validityPeriodFormat: 0x00 // not used 20 | }); 21 | } 22 | } 23 | 24 | export type ReportParams = { 25 | replyPath?: number; 26 | userDataHeader?: number; 27 | statusReportRequest?: number; 28 | mms?: number; 29 | }; 30 | -------------------------------------------------------------------------------- /src/utils/Type/DeliverType.ts: -------------------------------------------------------------------------------- 1 | import { PDUType } from './PDUType'; 2 | 3 | /** 4 | * Represents the PDU type for an SMS-DELIVER message in GSM SMS messaging. 5 | */ 6 | export class DeliverType extends PDUType { 7 | readonly messageTypeIndicator = PDUType.SMS_DELIVER; 8 | 9 | /** 10 | * Constructs a DeliverType instance. 11 | * @param params Parameters for configuring the DeliverType instance 12 | */ 13 | constructor(params: DeliverParams = {}) { 14 | super({ 15 | replyPath: params.replyPath ? 1 & params.replyPath : 0, 16 | userDataHeader: params.userDataHeader ? 1 & params.userDataHeader : 0, 17 | statusReportRequest: params.statusReportRequest ? 1 & params.statusReportRequest : 0, 18 | rejectDuplicates: params.mms ? 1 & params.mms : 0, 19 | validityPeriodFormat: 0x00 // not used 20 | }); 21 | } 22 | } 23 | 24 | export type DeliverParams = { 25 | replyPath?: number; 26 | userDataHeader?: number; 27 | statusReportRequest?: number; 28 | mms?: number; 29 | }; 30 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import prettierConfig from 'eslint-config-prettier'; 3 | import tseslint from 'typescript-eslint'; 4 | 5 | export default tseslint.config( 6 | { 7 | ignores: ['*', '!src', '!tests'] 8 | }, 9 | eslint.configs.recommended, 10 | ...tseslint.configs.recommendedTypeChecked, 11 | { 12 | languageOptions: { 13 | parserOptions: { 14 | project: true, 15 | tsconfigRootDir: import.meta.dirname 16 | } 17 | } 18 | }, 19 | { 20 | files: ['*.js'], 21 | ...tseslint.configs.disableTypeChecked 22 | }, 23 | { 24 | rules: { 25 | eqeqeq: 'error', 26 | curly: 'error', 27 | yoda: 'error', 28 | 'linebreak-style': ['error', 'unix'], 29 | 'space-infix-ops': 'error', 30 | 'space-unary-ops': 'error', 31 | '@typescript-eslint/consistent-type-imports': 'error', 32 | '@typescript-eslint/consistent-type-exports': 'error', 33 | '@typescript-eslint/no-import-type-side-effects': 'error' 34 | } 35 | }, 36 | prettierConfig 37 | ); 38 | -------------------------------------------------------------------------------- /src/utils/Type/SubmitType.ts: -------------------------------------------------------------------------------- 1 | import { PDUType } from './PDUType'; 2 | 3 | /** 4 | * Represents the PDU type for an SMS-SUBMIT message in GSM SMS messaging. 5 | */ 6 | export class SubmitType extends PDUType { 7 | readonly messageTypeIndicator = PDUType.SMS_SUBMIT; 8 | 9 | /** 10 | * Constructs a SubmitType instance. 11 | * @param params Parameters for configuring the SubmitType instance 12 | */ 13 | constructor(params: SubmitParams = {}) { 14 | super({ 15 | replyPath: params.replyPath ? 1 & params.replyPath : 0, 16 | userDataHeader: params.userDataHeader ? 1 & params.userDataHeader : 0, 17 | statusReportRequest: params.statusReportRequest ? 1 & params.statusReportRequest : 0, 18 | validityPeriodFormat: params.validityPeriodFormat ? 3 & params.validityPeriodFormat : 0, 19 | rejectDuplicates: params.rejectDuplicates ? 1 & params.rejectDuplicates : 0 20 | }); 21 | } 22 | } 23 | 24 | export type SubmitParams = { 25 | replyPath?: number; 26 | userDataHeader?: number; 27 | statusReportRequest?: number; 28 | validityPeriodFormat?: number; 29 | rejectDuplicates?: number; 30 | }; 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Julian Wowra (development@julianwowra.de) 4 | Copyright (c) 2023 jackkum (jackkum@bk.ru) 5 | 6 | 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: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | 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. 11 | -------------------------------------------------------------------------------- /.github/workflows/ReleaseNPM.yml: -------------------------------------------------------------------------------- 1 | name: ☁️ Release NPM 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - 'v*' 8 | 9 | jobs: 10 | npm: 11 | name: ☁️ Release NPM 12 | runs-on: ubuntu-latest 13 | 14 | permissions: 15 | id-token: write 16 | contents: write 17 | 18 | steps: 19 | - name: 📥 Checkout code 20 | uses: actions/checkout@v4 21 | 22 | - name: 📦 Install pnpm 23 | uses: pnpm/action-setup@v4 24 | 25 | - name: 🛠️ Setup Node.js 26 | uses: actions/setup-node@v4 27 | with: 28 | node-version: 'latest' 29 | cache: 'pnpm' 30 | 31 | - name: ⚡ Install dependencies 32 | run: pnpm install --frozen-lockfile 33 | 34 | - name: 🔨 Build library 35 | run: pnpm build:lib 36 | 37 | - name: 📦 Pack library 38 | run: pnpm pack -out package.tgz 39 | 40 | - name: ☁️ Publish to NPM 41 | uses: JS-DevTools/npm-publish@v3 42 | with: 43 | token: ${{ secrets.NPM_TOKEN }} 44 | 45 | - name: ☁️ Create a release 46 | uses: softprops/action-gh-release@v2 47 | if: startsWith(github.ref, 'refs/tags/') 48 | env: 49 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | with: 51 | name: Release ${{ github.ref_name }} 52 | files: package.tgz 53 | -------------------------------------------------------------------------------- /.github/workflows/PublishDocs.yml: -------------------------------------------------------------------------------- 1 | name: 📖 Publish documentation 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [main] 7 | 8 | concurrency: 9 | group: 'pages' 10 | cancel-in-progress: false 11 | 12 | jobs: 13 | docs: 14 | name: 📖 Publish documentation 15 | runs-on: ubuntu-latest 16 | 17 | permissions: 18 | contents: read 19 | pages: write 20 | id-token: write 21 | 22 | environment: 23 | name: github-pages 24 | url: ${{ steps.deployment.outputs.page_url }} 25 | 26 | steps: 27 | - name: 📥 Checkout code 28 | uses: actions/checkout@v4 29 | 30 | - name: 📦 Install pnpm 31 | uses: pnpm/action-setup@v4 32 | 33 | - name: 🛠️ Setup Node.js 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version: 'latest' 37 | cache: 'pnpm' 38 | 39 | - name: ⚡ Install dependencies 40 | run: pnpm install --frozen-lockfile 41 | 42 | - name: 🔨 Build library and docs 43 | run: pnpm build 44 | 45 | - name: ⚙️ Configure pages 46 | uses: actions/configure-pages@v4 47 | 48 | - name: 📡 Upload artifact 49 | uses: actions/upload-pages-artifact@v3 50 | with: 51 | path: docs 52 | 53 | - name: ☁️ Publish documentation 54 | id: deployment 55 | uses: actions/deploy-pages@v4 56 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, type Options } from 'tsup'; 2 | import pkg from './package.json'; 3 | 4 | const bannerComment = `/* 5 | * 📦 ${pkg.name} 6 | * 7 | * 🏷️ Version: ${pkg.version} 8 | * 📄 License: ${pkg.license} 9 | * 🕒 Build: ${new Date().toISOString()} 10 | * 🔗 Repository: ${pkg.repository.url} 11 | * 👤 Author: ${pkg.author} 12 | * 👤 Maintainer: Julian Wowra 13 | */\n`; 14 | 15 | const sharedConfig: Options = { 16 | tsconfig: './tsconfig.json', 17 | entry: ['src/index.ts'], 18 | outDir: 'dist', 19 | target: 'es2022', 20 | 21 | clean: true, 22 | minify: true, 23 | treeshake: true, 24 | sourcemap: true, 25 | skipNodeModulesBundle: true, 26 | banner: { 27 | js: bannerComment 28 | } 29 | }; 30 | 31 | export default defineConfig([ 32 | // ESM 33 | { 34 | ...sharedConfig, 35 | format: 'esm', 36 | platform: 'neutral', 37 | outDir: sharedConfig.outDir + '/esm', 38 | splitting: true, 39 | dts: false, 40 | outExtension: () => ({ js: '.mjs' }) 41 | }, 42 | 43 | // CommonJS 44 | { 45 | ...sharedConfig, 46 | format: 'cjs', 47 | platform: 'node', 48 | outDir: sharedConfig.outDir + '/cjs', 49 | splitting: false, 50 | dts: false, 51 | outExtension: () => ({ js: '.cjs' }) 52 | }, 53 | 54 | // Types 55 | { 56 | ...sharedConfig, 57 | outDir: sharedConfig.outDir + '/types', 58 | splitting: false, 59 | dts: { 60 | banner: bannerComment, 61 | only: true 62 | }, 63 | outExtension: () => ({ dts: '.d.ts' }) 64 | } 65 | ]); 66 | -------------------------------------------------------------------------------- /src/parse/parseUtils/parseType.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from '../../utils/Helper'; 2 | import { DeliverType } from '../../utils/Type/DeliverType'; 3 | import { PDUType } from '../../utils/Type/PDUType'; 4 | import { ReportType } from '../../utils/Type/ReportType'; 5 | import { SubmitType } from '../../utils/Type/SubmitType'; 6 | import type { GetSubstr } from '../index'; 7 | 8 | /** 9 | * Parses the PDU type from the provided PDU string. 10 | * This function extracts and constructs an appropriate PDU type object based on the type indicator byte in the PDU string. 11 | * 12 | * @param getPduSubstr A function to extract substrings from the PDU string 13 | * 14 | * @returns An instance of DeliverType, SubmitType, or ReportType representing the parsed PDU type 15 | * @throws Throws an error if an unknown SMS type is encountered 16 | */ 17 | export default function parseType(getPduSubstr: GetSubstr) { 18 | const byte = Helper.getByteFromHex(getPduSubstr(2)); 19 | 20 | const params = { 21 | replyPath: 1 & (byte >> 7), 22 | userDataHeader: 1 & (byte >> 6), 23 | statusReportRequest: 1 & (byte >> 5), 24 | validityPeriodFormat: 3 & (byte >> 3), 25 | rejectDuplicates: 1 & (byte >> 2), 26 | messageTypeIndicator: 3 & byte 27 | }; 28 | 29 | switch (3 & byte) { 30 | case PDUType.SMS_DELIVER: 31 | return new DeliverType(params); 32 | case PDUType.SMS_SUBMIT: 33 | return new SubmitType(params); 34 | case PDUType.SMS_REPORT: 35 | return new ReportType(params); 36 | default: 37 | throw new Error('node-pdu: Unknown SMS type!'); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/creating.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { Submit } from '../src/index'; 3 | 4 | describe('Test PDU creation', () => { 5 | test('should correctly create a single-part Submit PDU message', () => { 6 | const submit = new Submit('+1234567890', 'Hello, this is a simple Submit.'); 7 | 8 | const pduParts = submit.getPartStrings(); 9 | expect(pduParts).toHaveLength(1); 10 | 11 | expect(pduParts[0]).toBe('0001000A91214365870900001FC8329BFD6681E8E8F41C949E83C2A079BA0D679741D3BAB89DA6BB00'); 12 | }); 13 | 14 | test('should correctly create a multi-part Submit PDU message', () => { 15 | const submit = new Submit( 16 | '+1234567890', 17 | 'Hello, this is a long text to reproduce the issue that Adam Smid has provided, I am trying to reproduce this with success. I hope you guys have a good day today, make every day count in your live!' 18 | ); 19 | 20 | const pduParts = submit.getPartStrings(); 21 | expect(pduParts).toHaveLength(2); 22 | 23 | // Expression because of random message reference number (only on multiple part messages - header) 24 | expect(pduParts[0]).toMatch( 25 | /^0041000A91214365870900008D060804[\dA-F]{6}01C8329BFD6681E8E8F41C949E83C220F6DB7D06D1CB783A88FE06C9CB70F99B5C1F9741747419949ECFEB65101D1DA68382E4701B346DA7C92074780E82CBDFF634B94C668192A0701B4497E7D3EE3388FE06C9CB70F99B5C1F974174747A0EBAA7E968D0BC3E1E97E77317280942BFE16550FE5D07$/i 26 | ); 27 | expect(pduParts[1]).toMatch( 28 | /^0041000A912143658709000047060804[\dA-F]{6}02A0733D3F07A1C3F632280C3ABFDF6410399C07D1DFE4709E056A87D76550D95E96E741E4701E347ED7DD7450DA0DCABFEB72103B6D2F8700$/i 29 | ); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-pdu", 3 | "version": "2.1.1", 4 | "description": "Creates and parses SMS PDU strings", 5 | "main": "./dist/cjs/index.cjs", 6 | "module": "./dist/esm/index.mjs", 7 | "types": "./dist/types/index.d.ts", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/esm/index.mjs", 11 | "require": "./dist/cjs/index.cjs", 12 | "types": "./dist/types/index.d.ts" 13 | } 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/JulianWowra/node-pdu.git" 18 | }, 19 | "homepage": "https://github.com/JulianWowra/node-pdu", 20 | "license": "MIT", 21 | "author": "jackkum ", 22 | "publishConfig": { 23 | "provenance": true 24 | }, 25 | "packageManager": "pnpm@10.13.1", 26 | "devDependencies": { 27 | "@eslint/js": "^9.30.1", 28 | "eslint": "9.31.0", 29 | "eslint-config-prettier": "10.1.5", 30 | "prettier": "3.6.2", 31 | "tsup": "^8.5.0", 32 | "typedoc": "~0.28.7", 33 | "typedoc-github-theme": "~0.3.0", 34 | "typescript": "~5.8.3", 35 | "typescript-eslint": "8.36.0", 36 | "vitest": "^3.2.4" 37 | }, 38 | "scripts": { 39 | "build": "pnpm build:lib && pnpm build:docs", 40 | "build:lib": "tsup", 41 | "build:docs": "typedoc src --plugin typedoc-github-theme", 42 | "dev": "tsup --watch", 43 | "lint": "pnpm lint:format && pnpm lint:code && pnpm lint:tests", 44 | "lint:format": "prettier --check .", 45 | "lint:code": "eslint src", 46 | "lint:tests": "eslint tests", 47 | "test": "vitest run --dir ./tests/", 48 | "test:watch": "vitest watch --dir ./tests/" 49 | }, 50 | "pnpm": { 51 | "onlyBuiltDependencies": [ 52 | "esbuild" 53 | ] 54 | }, 55 | "keywords": [ 56 | "pdu", 57 | "sms" 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /src/parse/parseUtils/parseVP.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from '../../utils/Helper'; 2 | import { PDUType } from '../../utils/Type/PDUType'; 3 | import type { SubmitType } from '../../utils/Type/SubmitType'; 4 | import { VP } from '../../utils/VP'; 5 | import type { GetSubstr } from '../index'; 6 | import parseSCTS from './parseSCTS'; 7 | 8 | /** 9 | * Parses the validity period (VP) from a PDU string based on the specified validity period format (VPF). 10 | * This function is used specifically for parsing the validity period of a Submit type PDU. 11 | * 12 | * @param type The SubmitType instance specifying the validity period format (VPF) 13 | * @param getPduSubstr A function to extract substrings from the PDU string 14 | * 15 | * @returns An instance of VP representing the parsed validity period 16 | * @throws Throws an error if an unknown validity period format (VPF) is encountered 17 | */ 18 | export default function parseVP(type: SubmitType, getPduSubstr: GetSubstr) { 19 | const vpf = type.validityPeriodFormat; 20 | const vp = new VP(); 21 | 22 | if (vpf === PDUType.VPF_NONE) { 23 | return vp; 24 | } 25 | 26 | if (vpf === PDUType.VPF_ABSOLUTE) { 27 | const scts = parseSCTS(getPduSubstr); 28 | vp.setDateTime(scts.getIsoString()); 29 | 30 | return vp; 31 | } 32 | 33 | if (vpf === PDUType.VPF_RELATIVE) { 34 | const byte = Helper.getByteFromHex(getPduSubstr(2)); 35 | 36 | if (byte <= 0x8f) { 37 | vp.setInterval((byte + 1) * (5 * 60)); 38 | return vp; 39 | } 40 | 41 | if (byte <= 0xa7) { 42 | vp.setInterval(3600 * 24 * 12 + (byte - 0x8f) * (30 * 60)); 43 | return vp; 44 | } 45 | 46 | if (byte <= 0xc4) { 47 | vp.setInterval((byte - 0xa7) * (3600 * 24)); 48 | return vp; 49 | } 50 | 51 | vp.setInterval((byte - 0xc4) * (3600 * 24 * 7)); 52 | return vp; 53 | } 54 | 55 | throw new Error('node-pdu: Unknown validity period format!'); 56 | } 57 | -------------------------------------------------------------------------------- /.github/workflows/CheckCode.yml: -------------------------------------------------------------------------------- 1 | name: 🔎 Check code 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - '*' 8 | - '!draft/*' 9 | pull_request: 10 | types: [opened, synchronize] 11 | 12 | jobs: 13 | test: 14 | name: 🧪 Run Tests 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: 📥 Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: 📦 Install pnpm 22 | uses: pnpm/action-setup@v4 23 | 24 | - name: 🛠️ Setup Node.js 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: 'latest' 28 | cache: 'pnpm' 29 | 30 | - name: ⚡ Install dependencies 31 | run: pnpm install --frozen-lockfile 32 | 33 | - name: 🧪 Run Tests 34 | run: pnpm test 35 | 36 | build: 37 | name: 🏗️ Build project 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - name: 📥 Checkout code 42 | uses: actions/checkout@v4 43 | 44 | - name: 📦 Install pnpm 45 | uses: pnpm/action-setup@v4 46 | 47 | - name: 🛠️ Setup Node.js 48 | uses: actions/setup-node@v4 49 | with: 50 | node-version: 'latest' 51 | cache: 'pnpm' 52 | 53 | - name: ⚡ Install dependencies 54 | run: pnpm install --frozen-lockfile 55 | 56 | - name: 🔨 Build library 57 | run: pnpm build:lib 58 | 59 | lint: 60 | name: 🔍 Lint code 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - name: 📥 Checkout code 65 | uses: actions/checkout@v4 66 | 67 | - name: 📦 Install pnpm 68 | uses: pnpm/action-setup@v4 69 | 70 | - name: 🛠️ Setup Node.js 71 | uses: actions/setup-node@v4 72 | with: 73 | node-version: 'latest' 74 | cache: 'pnpm' 75 | 76 | - name: ⚡ Install dependencies 77 | run: pnpm install --frozen-lockfile 78 | 79 | - name: 🔎 Run linter 80 | run: pnpm lint 81 | -------------------------------------------------------------------------------- /src/parse/parseUtils/parseDCS.ts: -------------------------------------------------------------------------------- 1 | import { DCS, type DCSOptions } from '../../utils/DCS'; 2 | import { Helper } from '../../utils/Helper'; 3 | import type { GetSubstr } from '../index'; 4 | 5 | /** 6 | * Parses Data Coding Scheme (DCS) information from a PDU substring extractor. 7 | * This function extracts and constructs a DCS object from the provided PDU substring. 8 | * 9 | * @param getPduSubstr A function to extract substrings from the PDU string 10 | * @returns A DCS object representing the parsed Data Coding Scheme information 11 | */ 12 | export default function parseDCS(getPduSubstr: GetSubstr) { 13 | const byte = Helper.getByteFromHex(getPduSubstr(2)); 14 | const dcsOptions: DCSOptions = {}; 15 | 16 | dcsOptions.encodeGroup = 0x0f & (byte >> 4); 17 | dcsOptions.dataEncoding = 0x0f & byte; 18 | 19 | dcsOptions.textAlphabet = 3 & (dcsOptions.dataEncoding >> 2); 20 | dcsOptions.classMessage = 3 & dcsOptions.dataEncoding; 21 | 22 | switch (dcsOptions.encodeGroup) { 23 | case 0x0c: 24 | dcsOptions.discardMessage = true; 25 | dcsOptions.textAlphabet = DCS.ALPHABET_DEFAULT; 26 | break; 27 | 28 | case 0x0d: 29 | dcsOptions.storeMessage = true; 30 | break; 31 | 32 | case 0x0e: 33 | dcsOptions.storeMessageUCS2 = true; 34 | break; 35 | 36 | case 0x0f: 37 | dcsOptions.dataCodingAndMessageClass = true; 38 | 39 | if (dcsOptions.dataEncoding & (1 << 2)) { 40 | dcsOptions.textAlphabet = DCS.ALPHABET_8BIT; 41 | } 42 | 43 | break; 44 | 45 | default: 46 | dcsOptions.useMessageClass = !!(dcsOptions.encodeGroup & (1 << 0)); 47 | dcsOptions.compressedText = !!(dcsOptions.encodeGroup & (1 << 1)); 48 | } 49 | 50 | if (dcsOptions.discardMessage || dcsOptions.storeMessage || dcsOptions.storeMessageUCS2) { 51 | if (dcsOptions.dataEncoding & (1 << 3)) { 52 | dcsOptions.messageIndication = 1; 53 | dcsOptions.messageIndicationType = 3 & dcsOptions.dataEncoding; 54 | } 55 | } 56 | 57 | return new DCS(dcsOptions); 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Protocol Description Unit 2 | 3 | - 👉🏻 This package provides functions to parse and generate **Protocol Description Unit** (PDU) for the **Short Message Service** (SMS). 4 | - ✉️ The use case is to **send and receive SMS messages** via **GSM modems** or mobile phones. 5 | - 🏃 It is written in **TypeScript** and can be used in **Node.js and browser environments**. 6 | 7 | [![NPM](https://nodei.co/npm/node-pdu.png)](https://npmjs.org/package/node-pdu) 8 | 9 | ## Install 10 | 11 | ```sh 12 | npm install node-pdu 13 | ``` 14 | 15 | ## Usage 16 | 17 | Usage in TypeScript/JavaScript (with ES Modules): 18 | 19 | ```typescript 20 | import { Deliver, parse, Submit } from 'node-pdu'; 21 | 22 | /* 23 | * Parse a PDU string 24 | */ 25 | 26 | const str = '07919730071111F1000B919746121611F10000811170021222230DC8329BFD6681EE6F399B1C02'; 27 | const out = parse(str); 28 | 29 | if (out instanceof Deliver) { 30 | console.log(out.data.getText()); 31 | // Output: "Hello, world!" 32 | } 33 | 34 | /* 35 | * Generate a PDU string 36 | */ 37 | 38 | const address = '+999999999999'; 39 | const data = 'Hello everyone!'; 40 | 41 | const submit = new Submit(address, data); 42 | 43 | console.log(submit.toString()); 44 | // Output: "0001000C9199999999999900000FC8329BFD0695ED6579FEED2E8700" 45 | ``` 46 | 47 | ## 📚 Full documentation 48 | 49 | Click [here](https://julianwowra.github.io/node-pdu/). 50 | 51 | ## 🧪 Test script 52 | 53 | A small script allows you to scan the library for significant errors. 54 | 55 | Clone the repository, [enable pnpm](https://pnpm.io/installation#using-corepack), download the dependencies (`pnpm i`) and run: 56 | 57 | ```sh 58 | pnpm test 59 | ``` 60 | 61 | --- 62 | 63 | ## ❤️ Contributors 64 | 65 | **Thanks to these people who have contributed to this project:** 66 | 67 | [![Contributors](https://contrib.rocks/image?repo=julianwowra/node-pdu)](https://github.com/julianwowra/node-pdu/graphs/contributors) 68 | 69 | Made with [contrib.rocks](https://contrib.rocks). 70 | -------------------------------------------------------------------------------- /src/parse/parseUtils/parseSCA.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from '../../utils/Helper'; 2 | import { SCA } from '../../utils/SCA/SCA'; 3 | import { SCAType } from '../../utils/SCA/SCAType'; 4 | import type { GetSubstr } from '../index'; 5 | 6 | /** 7 | * Parses the Service Center Address (SCA) from a given substring extractor. 8 | * This function extracts and constructs an SCA object from the provided PDU string parts. 9 | * 10 | * @param getPduSubstr A function to extract substrings from the PDU string 11 | * @param isAddress Indicates whether the SCA represents an address (OA or DA) 12 | * 13 | * @returns An instance of SCA containing the parsed SCA information 14 | */ 15 | export default function parseSCA(getPduSubstr: GetSubstr, isAddress: boolean) { 16 | const size = Helper.getByteFromHex(getPduSubstr(2)); 17 | const sca = new SCA(isAddress); 18 | let octets; 19 | 20 | if (!size) { 21 | return sca; 22 | } 23 | 24 | // if is OA or DA then the size in semi-octets 25 | let adjustedSize = size; 26 | if (isAddress) { 27 | octets = Math.ceil(adjustedSize / 2); // to full octets 28 | // else size in octets 29 | } else { 30 | adjustedSize--; 31 | octets = adjustedSize; 32 | adjustedSize *= 2; // to semi-octets for future usage 33 | } 34 | 35 | const typeValue = Helper.getByteFromHex(getPduSubstr(2)); 36 | const type = new SCAType(typeValue); 37 | const hex = getPduSubstr(octets * 2); 38 | 39 | sca.type.setType(type.type); 40 | sca.type.setPlan(type.plan); 41 | 42 | if (sca.type.type === SCAType.TYPE_ALPHANUMERICAL) { 43 | const septets = Math.floor((adjustedSize * 4) / 7); // semi-octets to septets 44 | return sca.setPhone(Helper.decode7Bit(hex, septets), false, !isAddress); 45 | } 46 | 47 | // Detect padding char 48 | if (!isAddress && hex.charAt(adjustedSize - 2) === 'F') { 49 | adjustedSize--; 50 | } 51 | 52 | const phone = (hex.match(/.{1,2}/g) || []) 53 | .map((b) => SCA.mapFilterDecode(b).split('').reverse().join('')) 54 | .join('') 55 | .slice(0, adjustedSize); 56 | 57 | return sca.setPhone(phone, false, !isAddress); 58 | } 59 | -------------------------------------------------------------------------------- /src/parse/parseUtils/parseSCTS.ts: -------------------------------------------------------------------------------- 1 | import { SCTS } from '../../utils/SCTS'; 2 | import type { GetSubstr } from '../index'; 3 | 4 | /** 5 | * Parses the Service Center Time Stamp (SCTS) from a given substring extractor. 6 | * This function extracts and constructs an SCTS object representing the date and time provided in the PDU string. 7 | * 8 | * @param getPduSubstr A function to extract substrings from the PDU string 9 | * 10 | * @returns An instance of SCTS containing the parsed date and time information 11 | * @throws Throws an error if there are not enough bytes to parse or if parsing fails 12 | */ 13 | export default function parseSCTS(getPduSubstr: GetSubstr) { 14 | const hex = getPduSubstr(14); 15 | const params: number[] = []; 16 | 17 | if (!hex) { 18 | throw new Error('node-pdu: Not enough bytes!'); 19 | } 20 | 21 | (hex.match(/.{1,2}/g) || []).map((s) => { 22 | // NB: 7'th element (index = 6) is TimeZone and it can be a HEX 23 | if ((params.length < 6 && /\D+/.test(s)) || (params.length === 6 && /[^0-9A-Fa-f]/.test(s))) { 24 | params.push(0); 25 | return; 26 | } 27 | 28 | params.push(parseInt(s.split('').reverse().join(''), params.length < 6 ? 10 : 16)); 29 | }); 30 | 31 | if (params.length < 6) { 32 | throw new Error('node-pdu: Parsing failed!'); 33 | } 34 | 35 | // Parse TimeZone field (see 3GPP TS 23.040 section 9.2.3.11) 36 | let tzOff = params[6] & 0x7f; 37 | tzOff = (tzOff >> 4) * 10 + (tzOff & 0x0f); // Semi-octet to int 38 | tzOff = tzOff * 15; // Quarters of an hour to minutes 39 | 40 | // Check sign 41 | if (params[6] & 0x80) { 42 | tzOff *= -1; 43 | } 44 | 45 | // Build ISO8601 datetime 46 | const isoTime = new Date( 47 | Date.UTC( 48 | // Year 49 | params[0] > 70 ? 1900 + params[0] : 2000 + params[0], 50 | // Month 51 | params[1] - 1, 52 | // Day 53 | params[2], 54 | // Hour 55 | params[3], 56 | // Minute 57 | params[4], 58 | // Secound 59 | params[5] 60 | ) 61 | ); 62 | 63 | isoTime.setUTCMinutes(isoTime.getUTCMinutes() - tzOff); 64 | 65 | return new SCTS(isoTime, tzOff); 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/Data/Part.ts: -------------------------------------------------------------------------------- 1 | import type { Deliver } from '../../Deliver'; 2 | import type { Submit } from '../../Submit'; 3 | import { Helper } from '../Helper'; 4 | import type { Header } from './Header'; 5 | 6 | /** 7 | * Represents a part of a segmented SMS message. 8 | * 9 | * Used for messages that exceed the size limit for a single SMS and must be split. Each part contains 10 | * a segment of the full message, facilitating the reassembly of the complete message by the recipient. 11 | */ 12 | export class Part { 13 | readonly data: string; 14 | readonly size: number; 15 | readonly text: string; 16 | readonly header: Header | null; 17 | 18 | /** 19 | * Constructs a new Part instance for a segmented SMS message. 20 | * 21 | * @param data The raw data of this message part 22 | * @param size The size of this part, including headers and content 23 | * @param text The decoded text content of this part 24 | * @param header An optional header for concatenated message support 25 | */ 26 | constructor(data: string, size: number, text: string, header?: Header) { 27 | this.data = data; 28 | this.size = size; 29 | this.text = text; 30 | this.header = header || null; 31 | } 32 | 33 | /* 34 | * ================================================ 35 | * Private functions 36 | * ================================================ 37 | */ 38 | 39 | private getPduString(pdu: Deliver | Submit) { 40 | return pdu.getStart(); 41 | } 42 | 43 | private getPartSize() { 44 | return Helper.toStringHex(this.size); 45 | } 46 | 47 | /* 48 | * ================================================ 49 | * Public functions 50 | * ================================================ 51 | */ 52 | 53 | /** 54 | * Generates a string representation of this SMS part, suitable for transmission. 55 | * 56 | * This method constructs the PDU string for this part by concatenating the PDU start, 57 | * the size of the part, any headers, and the data content. 58 | * 59 | * @param pdu The PDU (either Deliver or Submit) that this part belongs to. 60 | * @returns A string representing the PDU for this part of the message. 61 | */ 62 | toString(pdu: Deliver | Submit) { 63 | // concate pdu, size of part, headers, data 64 | return this.getPduString(pdu) + this.getPartSize() + (this.header ? this.header.toString() : '') + this.data; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/utils/checkPdu.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'vitest'; 2 | import { Deliver, Report, Submit, utils } from '../../src/index'; 3 | 4 | export function expectDeliver(pdu: utils.PDU): asserts pdu is Deliver { 5 | expect(pdu).instanceOf(Deliver); 6 | } 7 | 8 | export function expectReport(pdu: utils.PDU): asserts pdu is Report { 9 | expect(pdu).instanceOf(Report); 10 | } 11 | 12 | export function expectSubmit(pdu: utils.PDU): asserts pdu is Submit { 13 | expect(pdu).instanceOf(Submit); 14 | } 15 | 16 | export function expectDeliverOrSubmit(pdu: utils.PDU): asserts pdu is Deliver | Submit { 17 | expect(pdu instanceof Deliver || pdu instanceof Submit, 'expects PDU to be an instance of Deliver or Submit').toBeTruthy(); 18 | } 19 | 20 | export function expectServiceCenterAddress(pdu: Deliver | Report | Submit, expecting: string) { 21 | expect(pdu.serviceCenterAddress.isAddress).toBeFalsy(); 22 | expect(pdu.serviceCenterAddress.phone).toBe(expecting); 23 | } 24 | 25 | export function expectAddress(pdu: Deliver | Report | Submit, expecting: string) { 26 | const isInternatinal = expecting.startsWith('00') || expecting.startsWith('+'); 27 | 28 | if (isInternatinal) { 29 | if (expecting.startsWith('+')) { 30 | expecting = expecting.substring(1); 31 | } 32 | 33 | if (expecting.startsWith('00')) { 34 | expecting = expecting.substring(2); 35 | } 36 | } 37 | 38 | expect(pdu.address.isAddress).toBeTruthy(); 39 | expect(pdu.address.phone).toBe(expecting); 40 | 41 | if (isInternatinal) { 42 | expect(pdu.address.type.type).toBe(utils.SCAType.TYPE_INTERNATIONAL); 43 | } 44 | } 45 | 46 | export function expectDataCodingScheme(pdu: Deliver | Report | Submit, expecting: number) { 47 | expect(pdu.dataCodingScheme.getValue()).toBe(expecting); 48 | } 49 | 50 | export function expectServiceCenterTimeStamp(pdu: Deliver, expecting: string) { 51 | expect(pdu.serviceCenterTimeStamp.getIsoString()).toBe(expecting); 52 | } 53 | 54 | export function expectUserDataHeader(pdu: Deliver | Submit, expecting: { current: number; pointer: number; segments: number }) { 55 | const header = pdu.data.parts[0]?.header; 56 | 57 | expect(header).instanceOf(utils.Header); 58 | expect(header?.getCurrent()).toBe(expecting.current); 59 | expect(header?.getPointer()).toBe(expecting.pointer); 60 | expect(header?.getSegments()).toBe(expecting.segments); 61 | } 62 | 63 | export function expectUserData(pdu: Deliver | Submit, expecting: { text: string; size?: number }) { 64 | expect(pdu.data.getText()).toBe(expecting.text); 65 | 66 | if (expecting.size !== undefined) { 67 | expect(pdu.data.size).toBe(expecting.size); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/7bit-encoding-decoding.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { utils } from '../src/index'; 3 | 4 | const tests = [ 5 | { 6 | name: 'should encode/decode lowercase letters', 7 | text: 'abcdefghijklmnopqrstuvwxyz', 8 | code: '61F1985C369FD169F59ADD76BFE171F99C5EB7DFF1793D' 9 | }, 10 | { 11 | name: 'should encode/decode uppercase letters', 12 | text: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 13 | code: '41E19058341E9149E592D9743EA151E9945AB55EB1592D' 14 | }, 15 | { 16 | name: 'should encode/decode digits', 17 | text: '0123456789', 18 | code: 'B0986C46ABD96EB81C' 19 | }, 20 | { 21 | name: 'should encode/decode 1 symbol', 22 | text: 'a', 23 | code: '61' 24 | }, 25 | { 26 | name: 'should encode/decode 2 symbols', 27 | text: 'ab', 28 | code: '6131' 29 | }, 30 | { 31 | name: 'should encode/decode 3 symbols', 32 | text: 'abc', 33 | code: '61F118' 34 | }, 35 | { 36 | name: 'should encode/decode 4 symbols', 37 | text: 'abcd', 38 | code: '61F1980C' 39 | }, 40 | { 41 | name: 'should encode/decode 5 symbols', 42 | text: 'abcde', 43 | code: '61F1985C06' 44 | }, 45 | { 46 | name: 'should encode/decode 6 symbols', 47 | text: 'abcdef', 48 | code: '61F1985C3603' 49 | }, 50 | { 51 | name: 'should encode/decode 7 symbols', 52 | text: 'abcdefg', 53 | code: '61F1985C369F01' 54 | }, 55 | { 56 | name: 'should encode/decode 8 symbols', 57 | text: 'abcdefgh', 58 | code: '61F1985C369FD1' 59 | }, 60 | { 61 | name: 'should encode/decode 9 symbols', 62 | text: 'abcdefghi', 63 | code: '61F1985C369FD169' 64 | }, 65 | { 66 | name: 'should ignore "@" during encoding (7-bit loss)', 67 | text: 'abcdefg@', 68 | code: '61F1985C369F01', 69 | codeLen: 8 70 | }, 71 | { 72 | name: 'should correctly decode final "}" character (escape test)', 73 | text: '{test}', 74 | code: '1B14BD3CA76F52' 75 | }, 76 | { 77 | name: 'should encode with 3-bit alignment', 78 | text: 'abc', 79 | code: '088BC7', 80 | alignBits: 3 81 | } 82 | ]; 83 | 84 | describe('7bit encoding', () => { 85 | test.each(tests)('$name', ({ text, alignBits, code, codeLen }) => { 86 | const { result, length } = utils.Helper.encode7Bit(text, alignBits); 87 | 88 | expect(result).toBe(code); 89 | 90 | if (codeLen) { 91 | expect(length).toBe(codeLen); 92 | } 93 | }); 94 | }); 95 | 96 | describe('7bit decoding', () => { 97 | test.each(tests)('$name', ({ code, codeLen, alignBits, text }) => { 98 | expect(utils.Helper.decode7Bit(code, codeLen, alignBits)).toBe(text); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/utils/SCTS.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from './Helper'; 2 | 3 | /** 4 | * Represents the Service Centre Time Stamp (SCTS) of an SMS message. 5 | * 6 | * Marks the time and date the SMSC received the message, used in delivery reports and incoming 7 | * messages for timing analysis and record-keeping, providing a temporal reference for the message's handling. 8 | */ 9 | export class SCTS { 10 | readonly time: number; 11 | readonly tzOff: number; 12 | 13 | /** 14 | * Constructs a Service Centre Time Stamp (SCTS) instance. 15 | * 16 | * @param date The date object representing the time and date the message was received 17 | * @param tzOff The time zone offset in minutes, defaults to the local time zone offset if not provided 18 | */ 19 | constructor(date: Date, tzOff?: number) { 20 | this.time = date.getTime() / 1000; 21 | this.tzOff = tzOff || -1 * date.getTimezoneOffset(); 22 | } 23 | 24 | /* 25 | * ================================================ 26 | * Private functions 27 | * ================================================ 28 | */ 29 | 30 | private getDateTime() { 31 | const tzAbs = Math.floor(Math.abs(this.tzOff) / 15); // To quarters of an hour 32 | const x = Math.floor(tzAbs / 10) * 16 + (tzAbs % 10) + (this.tzOff < 0 ? 0x80 : 0x00); 33 | 34 | return this.getDateWithOffset().toISOString().replace(/[-T:]/g, '').slice(2, 14) + Helper.toStringHex(x); 35 | } 36 | 37 | private getDateWithOffset() { 38 | return new Date(this.time * 1000 + this.tzOff * 60 * 1000); 39 | } 40 | 41 | /* 42 | * ================================================ 43 | * Public functions 44 | * ================================================ 45 | */ 46 | 47 | /** 48 | * Converts the SCTS to an ISO 8601 string format with a custom time zone offset. 49 | * 50 | * This method formats the date and time into an easily readable ISO 8601 format, 51 | * adjusting for the stored time zone offset to reflect the local time at the service centre. 52 | * 53 | * @returns The SCTS as an ISO 8601 formatted string with a custom time zone offset. 54 | */ 55 | getIsoString() { 56 | const datetime = this.getDateWithOffset() 57 | .toISOString() 58 | .replace(/.\d{3}Z$/, ''); 59 | 60 | const offset = Math.abs(this.tzOff / 60) 61 | .toString() 62 | .padStart(2, '0'); 63 | 64 | return datetime + (this.tzOff > 0 ? '+' : '-') + offset + ':00'; 65 | } 66 | 67 | /** 68 | * Converts the SCTS to a string representation based on the SMS PDU specifications. 69 | * 70 | * This method formats the service centre time stamp into a semi-octet representation suitable 71 | * for inclusion in a PDU message, converting the date and time components and including the 72 | * time zone in a standardized format. 73 | * 74 | * @returns The SCTS as a string formatted according to the SMS PDU specifications 75 | */ 76 | toString() { 77 | return (this.getDateTime().match(/.{1,2}/g) || []).map((s) => s.split('').reverse().join('')).join(''); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/utils/SCA/SCAType.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from '../Helper'; 2 | 3 | /** 4 | * Defines the Service Centre Address (SCA) format in SMS messages. 5 | * 6 | * Specifies the SCA format, crucial for SMS routing and delivery. Types may include international, 7 | * national, and others, ensuring compatibility across networks. 8 | */ 9 | export class SCAType { 10 | static readonly TYPE_UNKNOWN = 0x00; 11 | static readonly TYPE_INTERNATIONAL = 0x01; 12 | static readonly TYPE_NATIONAL = 0x02; 13 | static readonly TYPE_ACCEPTER_INTO_NET = 0x03; 14 | static readonly TYPE_SUBSCRIBER_NET = 0x04; 15 | static readonly TYPE_ALPHANUMERICAL = 0x05; 16 | static readonly TYPE_TRIMMED = 0x06; 17 | static readonly TYPE_RESERVED = 0x07; 18 | 19 | static readonly PLAN_UNKNOWN = 0x00; 20 | static readonly PLAN_ISDN = 0x01; 21 | static readonly PLAN_X_121 = 0x02; 22 | static readonly PLAN_TELEX = 0x03; 23 | static readonly PLAN_NATIONAL = 0x08; 24 | static readonly PLAN_INDIVIDUAL = 0x09; 25 | static readonly PLAN_ERMES = 0x0a; 26 | static readonly PLAN_RESERVED = 0x0f; 27 | 28 | private _type: number; 29 | private _plan: number; 30 | 31 | constructor(value = 0x91) { 32 | this._type = 0x07 & (value >> 4); 33 | this._plan = 0x0f & value; 34 | } 35 | 36 | /* 37 | * ================================================ 38 | * Getter & Setter 39 | * ================================================ 40 | */ 41 | 42 | /** 43 | * Retrieves the type of the Service Centre Address (SCA). 44 | * The type indicates the format or category of the SCA, such as international, national, or alphanumeric. 45 | * 46 | * @returns The type of the SCA 47 | */ 48 | get type() { 49 | return this._type; 50 | } 51 | 52 | /** 53 | * Sets the type of the Service Centre Address (SCA). 54 | * This method allows updating the type of the SCA, ensuring compatibility and proper routing. 55 | * 56 | * @param type The new type of the SCA 57 | * @returns The instance of this SCAType, allowing for method chaining 58 | */ 59 | setType(type: number) { 60 | this._type = 0x07 & type; 61 | return this; 62 | } 63 | 64 | /** 65 | * Retrieves the numbering plan identification of the Service Centre Address (SCA). 66 | * The plan indicates the numbering plan used for the SCA, such as ISDN, national, or individual. 67 | * 68 | * @returns The numbering plan identification of the SCA 69 | */ 70 | get plan() { 71 | return this._plan; 72 | } 73 | 74 | /** 75 | * Sets the numbering plan identification of the Service Centre Address (SCA). 76 | * This method allows updating the numbering plan used for the SCA, ensuring compatibility and proper routing. 77 | * 78 | * @param plan The new numbering plan identification for the SCA 79 | * @returns The instance of this SCAType, allowing for method chaining 80 | */ 81 | setPlan(plan: number) { 82 | this._plan = 0x0f & plan; 83 | return this; 84 | } 85 | 86 | /* 87 | * ================================================ 88 | * Public functions 89 | * ================================================ 90 | */ 91 | 92 | /** 93 | * Retrieves the numerical value representing the SCAType. 94 | * This value is used internally and for PDU encoding. 95 | * 96 | * @returns The numerical value representing the SCAType 97 | */ 98 | getValue() { 99 | return (1 << 7) | (this._type << 4) | this._plan; 100 | } 101 | 102 | /** 103 | * Converts the SCAType instance to its hexadecimal string representation. 104 | * 105 | * This method converts the SCAType instance to its hexadecimal string representation suitable for 106 | * inclusion in PDUs. 107 | * 108 | * @returns The hexadecimal string representation of the SCAType 109 | */ 110 | toString() { 111 | return Helper.toStringHex(this.getValue()); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /tests/appending.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { parse } from '../src/index'; 3 | import { expectDeliver, expectUserData } from './utils/checkPdu'; 4 | 5 | describe('PDU concatenation and message reassembly', () => { 6 | test('should correctly append simple concatenated messages (in order)', () => { 7 | const pduStr1 = '07919730071111F1400B919746121611F10000811170021222230E06080412340201C8329BFD6601'; 8 | const pduStr2 = '07919730071111F1400B919746121611F10000811170021232230F06080412340202A0FB5BCE268700'; 9 | 10 | const parsedPdu1 = parse(pduStr1); 11 | expectDeliver(parsedPdu1); 12 | 13 | const parsedPdu2 = parse(pduStr2); 14 | expectDeliver(parsedPdu2); 15 | 16 | parsedPdu1.data.append(parsedPdu2); 17 | expectUserData(parsedPdu1, { text: 'Hello, world!' }); 18 | }); 19 | 20 | test('should correctly append concatenated messages with reversed part order', () => { 21 | const pduStr1 = '07919730071111F1400B919746121611F10000811170021232230F06080412350202A0FB5BCE268700'; 22 | const pduStr2 = '07919730071111F1400B919746121611F10000811170021222230B06080412350201C8340B'; 23 | 24 | const parsedPdu1 = parse(pduStr1); 25 | expectDeliver(parsedPdu1); 26 | 27 | const parsedPdu2 = parse(pduStr2); 28 | expectDeliver(parsedPdu2); 29 | 30 | parsedPdu1.data.append(parsedPdu2); 31 | expectUserData(parsedPdu1, { text: 'Hi, world!' }); 32 | }); 33 | 34 | test.each([ 35 | { 36 | parts: '1 & 2', 37 | pduStr1: '07919730071111F1400B919746121611F10000811170021222230E060804123403015774987E9A03', 38 | pduStr2: '07919730071111F1400B919746121611F10000811170021232230C06080412340302A03A9C05', 39 | text: "What's up," 40 | }, 41 | { 42 | parts: '2 & 3', 43 | pduStr1: '07919730071111F1400B919746121611F10000811170021232230C06080412340302A03A9C05', 44 | pduStr2: '07919730071111F1400B919746121611F10000811170021242230D06080412340303A076D8FD03', 45 | text: ' up, man?' 46 | }, 47 | { 48 | parts: '1 & 3', 49 | pduStr1: '07919730071111F1400B919746121611F10000811170021222230E060804123403015774987E9A03', 50 | pduStr2: '07919730071111F1400B919746121611F10000811170021242230D06080412340303A076D8FD03', 51 | text: "What's man?" 52 | } 53 | ])('should append unsorted message parts ($parts)', ({ pduStr1, pduStr2, text }) => { 54 | const parsedPdu1 = parse(pduStr1); 55 | expectDeliver(parsedPdu1); 56 | 57 | const parsedPdu2 = parse(pduStr2); 58 | expectDeliver(parsedPdu2); 59 | 60 | parsedPdu1.data.append(parsedPdu2); 61 | expectUserData(parsedPdu1, { text }); 62 | }); 63 | 64 | test('should not duplicate user data when appending the same part twice', () => { 65 | const pduStr = '07919730071111F1400B919746121611F10000811170021222230E06080412340201C8329BFD6601'; 66 | 67 | const parsedPdu = parse(pduStr); 68 | expectDeliver(parsedPdu); 69 | 70 | // Appending the same part 71 | parsedPdu.data.append(parsedPdu); 72 | 73 | expectUserData(parsedPdu, { text: 'Hello,' }); 74 | }); 75 | 76 | test('should throw error when appending parts from different messages', () => { 77 | const pduStr1 = '07919730071111F1400B919746121611F10000811170021222230E06080412340201C8329BFD6601'; 78 | const pduStr2 = '07919730071111F1400B919746121611F10000811170021232230F06080412350202A0FB5BCE268700'; 79 | 80 | const parsedPdu1 = parse(pduStr1); 81 | expectDeliver(parsedPdu1); 82 | 83 | const parsedPdu2 = parse(pduStr2); 84 | expectDeliver(parsedPdu2); 85 | 86 | expect(() => parsedPdu1.data.append(parsedPdu2)).toThrowError('Part from different message!'); 87 | }); 88 | 89 | test('should throw error when appending parts with collided identifiers', () => { 90 | const pduStr1 = '07919730071111F1400B919746121611F10000811170021222230E06080412340201C8329BFD6601'; 91 | const pduStr2 = '07919730071111F1400B919746121611F10000811170021232230C06080412340302A03A9C05'; 92 | 93 | const parsedPdu1 = parse(pduStr1); 94 | expectDeliver(parsedPdu1); 95 | 96 | const parsedPdu2 = parse(pduStr2); 97 | expectDeliver(parsedPdu2); 98 | 99 | expect(() => parsedPdu1.data.append(parsedPdu2)).toThrowError('Part from different message!'); 100 | }); 101 | 102 | test('should correctly append concatenated message using 8-bit reference', () => { 103 | const pduStr1 = '07919730071111F1400B919746121611F10000100161916223230D0500032E020190E175DD1D06'; 104 | const pduStr2 = '07919730071111F1400B919746121611F10000100161916233230E0500032E020240ED303D4C0F03'; 105 | 106 | const parsedPdu1 = parse(pduStr1); 107 | expectDeliver(parsedPdu1); 108 | 109 | const parsedPdu2 = parse(pduStr2); 110 | expectDeliver(parsedPdu2); 111 | 112 | parsedPdu1.data.append(parsedPdu2); 113 | expectUserData(parsedPdu1, { text: 'Hakuna matata' }); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /src/parse/parseUtils/parseData.ts: -------------------------------------------------------------------------------- 1 | import { Data, type DataOptions } from '../../utils/Data/Data'; 2 | import { Header } from '../../utils/Data/Header'; 3 | import { Part } from '../../utils/Data/Part'; 4 | import { DCS } from '../../utils/DCS'; 5 | import { Helper } from '../../utils/Helper'; 6 | import type { PDUType } from '../../utils/Type/PDUType'; 7 | import type { GetSubstr } from '../index'; 8 | 9 | /** 10 | * Parses the user data portion of a PDU string into a Data object. 11 | * 12 | * This function extracts the user data from the PDU string, including any headers, decodes it according to the specified data coding scheme, 13 | * and constructs a Data object representing the parsed user data. 14 | * 15 | * @param type The type of the PDU, determining how the user data is parsed 16 | * @param dataCodingScheme The data coding scheme used to encode the user data 17 | * @param userDataLength The length of the user data in octets 18 | * @param getPduSubstr A function to extract substrings from the PDU string 19 | * 20 | * @returns A Data object containing the parsed user data 21 | */ 22 | export default function parseData(type: PDUType, dataCodingScheme: DCS, userDataLength: number, getPduSubstr: GetSubstr) { 23 | const dataOptions: DataOptions = {}; 24 | 25 | if (dataCodingScheme.textAlphabet === DCS.ALPHABET_UCS2) { 26 | dataOptions.isUnicode = true; 27 | } 28 | 29 | const tmp = parsePart(type, dataCodingScheme, userDataLength, getPduSubstr); 30 | 31 | dataOptions.data = tmp.text; 32 | dataOptions.size = tmp.size; 33 | dataOptions.parts = [tmp.part]; 34 | 35 | return new Data(dataOptions); 36 | } 37 | 38 | /** 39 | * Parses a single part of the user data from a PDU string. 40 | * 41 | * This function extracts a part of the user data from the PDU string, including any headers if present, 42 | * decodes it according to the specified data coding scheme, and constructs a Part object representing the parsed part. 43 | * 44 | * @param type The type of the PDU, determining how the user data is parsed 45 | * @param dataCodingScheme The data coding scheme used to encode the user data 46 | * @param userDataLength The length of the user data in octets 47 | * @param getPduSubstr A function to extract substrings from the PDU string 48 | * 49 | * @returns An object containing the parsed text, size, and Part object representing the parsed part 50 | */ 51 | function parsePart(type: PDUType, dataCodingScheme: DCS, userDataLength: number, getPduSubstr: GetSubstr) { 52 | let length = 0; 53 | 54 | if (dataCodingScheme.textAlphabet === DCS.ALPHABET_DEFAULT) { 55 | length = Math.ceil((userDataLength * 7) / 8); // Convert septets to octets 56 | } else { 57 | length = userDataLength; // Length already in octets 58 | } 59 | 60 | let header: Header | undefined; 61 | let hdrSz = 0; // Header full size: UDHL + UDH 62 | 63 | if (type.userDataHeader === 1) { 64 | header = parseHeader(getPduSubstr); 65 | hdrSz = 1 + header.getSize(); // UDHL field length + UDH length 66 | length -= hdrSz; 67 | } 68 | 69 | const hex = getPduSubstr(length * 2); // Extract Octets x2 chars 70 | 71 | const text = (() => { 72 | if (dataCodingScheme.textAlphabet === DCS.ALPHABET_DEFAULT) { 73 | const inLen = userDataLength - Math.ceil((hdrSz * 8) / 7); // Convert octets to septets 74 | const alignBits = Math.ceil((hdrSz * 8) / 7) * 7 - hdrSz * 8; 75 | 76 | return Helper.decode7Bit(hex, inLen, alignBits); 77 | } 78 | 79 | if (dataCodingScheme.textAlphabet === DCS.ALPHABET_8BIT) { 80 | return Helper.decode8Bit(hex); 81 | } 82 | 83 | if (dataCodingScheme.textAlphabet === DCS.ALPHABET_UCS2) { 84 | return Helper.decode16Bit(hex); 85 | } 86 | 87 | throw new Error('node-pdu: Unknown alpabet!'); 88 | })(); 89 | 90 | const part = new Part(hex, userDataLength, text, header); 91 | 92 | return { text, size: userDataLength, part }; 93 | } 94 | 95 | /** 96 | * Parses the user data header from a PDU string. 97 | * 98 | * This function extracts and parses the user data header (if present) from the PDU string, 99 | * constructing a Header object representing the parsed header information. 100 | * 101 | * @param getPduSubstr A function to extract substrings from the PDU string 102 | * @returns A Header object representing the parsed user data header 103 | */ 104 | function parseHeader(getPduSubstr: GetSubstr) { 105 | let udhl = Helper.getByteFromHex(getPduSubstr(2)); 106 | const ies: { type: number; dataHex: string }[] = []; 107 | 108 | /* 109 | * NB: this parser does not perform the IE data parsing, it only 110 | * splits the header onto separate IE(s) and then create a new Header 111 | * object using the extracted IE(s) as an initializer. IE data parsing 112 | * (if any) will beb performed later by the Header class constructor. 113 | */ 114 | 115 | // Parse IE(s) as TLV 116 | while (udhl > 0) { 117 | const ieType = Helper.getByteFromHex(getPduSubstr(2)); 118 | const ieLen = Helper.getByteFromHex(getPduSubstr(2)); 119 | 120 | ies.push({ type: ieType, dataHex: getPduSubstr(ieLen * 2) }); 121 | udhl -= 2 + ieLen; 122 | } 123 | 124 | return new Header(ies); 125 | } 126 | -------------------------------------------------------------------------------- /src/utils/VP.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from './Helper'; 2 | import type { PDU } from './PDU'; 3 | import { SCTS } from './SCTS'; 4 | import { PDUType } from './Type/PDUType'; 5 | 6 | /** 7 | * Represents the Validity Period (VP) of an SMS message. 8 | * 9 | * Defines how long an SMS is stored at the SMSC before delivery attempts cease. This duration 10 | * ensures messages remain relevant and can vary from minutes to days based on the sender's preference. 11 | */ 12 | export class VP { 13 | static readonly PID_ASSIGNED = 0x00; 14 | 15 | private _datetime: Date | null; 16 | private _interval: number | null; 17 | 18 | /** 19 | * Constructs a Validity Period (VP) instance. 20 | * @param options An object containing optional parameters for the VP instance 21 | */ 22 | constructor(options: VPOptions = {}) { 23 | this._datetime = options.datetime || null; 24 | this._interval = options.interval || null; 25 | } 26 | 27 | /* 28 | * ================================================ 29 | * Getter & Setter 30 | * ================================================ 31 | */ 32 | 33 | /** 34 | * Retrieves the datetime set for the Validity Period. 35 | * 36 | * This property represents the exact date and time the SMS should be considered valid. 37 | * If null, it implies that a specific datetime has not been set for this validity period. 38 | * 39 | * @returns The datetime as a Date object or null if not set 40 | */ 41 | get dateTime() { 42 | return this._datetime; 43 | } 44 | 45 | /** 46 | * Sets the datetime for the SMS Validity Period. 47 | * 48 | * This method allows setting a specific date and time until which the SMS is considered valid. 49 | * Can be set using a Date object or a string that can be parsed into a Date. 50 | * 51 | * @param datetime The datetime to set as a Date object or a string representation 52 | * @returns The instance of this VP, allowing for method chaining 53 | */ 54 | setDateTime(datetime: string | Date) { 55 | if (datetime instanceof Date) { 56 | this._datetime = datetime; 57 | return this; 58 | } 59 | 60 | this._datetime = new Date(Date.parse(datetime)); 61 | return this; 62 | } 63 | 64 | /** 65 | * Retrieves the interval set for the Validity Period. 66 | * 67 | * This property represents the duration in seconds for which the SMS should be considered valid. 68 | * If null, it implies that a specific interval has not been set for this validity period. 69 | * 70 | * @returns The interval in seconds or null if not set 71 | */ 72 | get interval() { 73 | return this._interval; 74 | } 75 | 76 | /** 77 | * Sets the interval for the SMS Validity Period. 78 | * 79 | * This method allows setting a specific duration in seconds until which the SMS is considered valid. 80 | * It's an alternative to setting a specific datetime. 81 | * 82 | * @param interval The interval in seconds to set for the validity period 83 | * @returns The instance of this VP, allowing for method chaining 84 | */ 85 | setInterval(interval: number) { 86 | this._interval = interval; 87 | return this; 88 | } 89 | 90 | /* 91 | * ================================================ 92 | * Public functions 93 | * ================================================ 94 | */ 95 | 96 | /** 97 | * Converts the Validity Period to a PDU string representation. 98 | * 99 | * This method generates a string suitable for inclusion in a PDU, based on the validity period's format. 100 | * It can handle both absolute and relative formats and adjusts the PDU's validity period format accordingly. 101 | * This is critical for ensuring that the SMS's validity period is correctly interpreted by the SMSC. 102 | * 103 | * @param pdu The PDU instance to which this validity period belongs 104 | * @returns A string representation of the validity period for inclusion in the PDU 105 | */ 106 | toString(pdu: PDU): string { 107 | const type = pdu.type; 108 | 109 | // absolute value 110 | if (this._datetime !== null) { 111 | type.setValidityPeriodFormat(PDUType.VPF_ABSOLUTE); 112 | 113 | return new SCTS(this._datetime).toString(); 114 | } 115 | 116 | // relative value in seconds 117 | if (this._interval) { 118 | type.setValidityPeriodFormat(PDUType.VPF_RELATIVE); 119 | 120 | const minutes = Math.ceil(this._interval / 60); 121 | const hours = Math.ceil(this._interval / 60 / 60); 122 | const days = Math.ceil(this._interval / 60 / 60 / 24); 123 | const weeks = Math.ceil(this._interval / 60 / 60 / 24 / 7); 124 | 125 | if (hours <= 12) { 126 | return Helper.toStringHex(Math.ceil(minutes / 5) - 1); 127 | } 128 | 129 | if (hours <= 24) { 130 | return Helper.toStringHex(Math.ceil((minutes - 720) / 30) + 143); 131 | } 132 | 133 | if (hours <= 30 * 24 * 3600) { 134 | return Helper.toStringHex(days + 166); 135 | } 136 | 137 | return Helper.toStringHex((weeks > 63 ? 63 : weeks) + 192); 138 | } 139 | 140 | // vpf not used 141 | type.setValidityPeriodFormat(PDUType.VPF_NONE); 142 | 143 | return ''; 144 | } 145 | } 146 | 147 | export type VPOptions = { 148 | datetime?: Date; 149 | interval?: number; 150 | }; 151 | -------------------------------------------------------------------------------- /src/utils/PID.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Represents the Protocol Identifier (PID) of an SMS message. 3 | * 4 | * Specifies the type or nature of the message, allowing the system to handle it appropriately. It can 5 | * indicate special types of messages such as voicemail notifications or system messages, among others. 6 | */ 7 | export class PID { 8 | static readonly PID_ASSIGNED = 0x00; // Assigns bits 0..5 as defined below 9 | static readonly PID_GSM_03_40 = 0x01; // See GSM 03.40 TP-PID complete definition 10 | static readonly PID_RESERVED = 0x02; // Reserved 11 | static readonly PID_SPECIFIC = 0x03; // Assigns bits 0-5 for SC specific use 12 | 13 | static readonly TYPE_IMPLICIT = 0x00; // Implicit 14 | static readonly TYPE_TELEX = 0x01; // Telex (or teletex reduced to telex format) 15 | static readonly TYPE_TELEFAX = 0x02; // Group 3 telefax 16 | static readonly TYPE_VOICE = 0x04; // Voice telephone (i.e. conversion to speech) 17 | static readonly TYPE_ERMES = 0x05; // ERMES (European Radio Messaging System) 18 | static readonly TYPE_NPS = 0x06; // National Paging system (known to the SC 19 | static readonly TYPE_X_400 = 0x11; // Any public X.400-based message handling system 20 | static readonly TYPE_IEM = 0x12; // Internet Electronic Mail 21 | 22 | private _pid: number; 23 | private _indicates: number; 24 | private _type: number; 25 | 26 | /** 27 | * Constructs a Protocol Identifier (PID) instance. 28 | * @param options An object containing optional parameters for the PID instance 29 | */ 30 | constructor(options: PIDOptions = {}) { 31 | this._pid = options.pid || PID.PID_ASSIGNED; 32 | this._indicates = options.indicates || 0x00; 33 | this._type = options.type || PID.TYPE_IMPLICIT; 34 | } 35 | 36 | /* 37 | * ================================================ 38 | * Getter & Setter 39 | * ================================================ 40 | */ 41 | 42 | /** 43 | * Retrieves the Protocol Identifier (PID) value. 44 | * 45 | * The PID value is crucial for identifying the nature and handling of the SMS message within 46 | * the network. It determines if the message is of a specific type, such as voicemail 47 | * notifications or system messages. 48 | * 49 | * @returns The current PID value 50 | */ 51 | get pid() { 52 | return this._pid; 53 | } 54 | 55 | /** 56 | * Sets the Protocol Identifier (PID) value. 57 | * 58 | * Allows specifying the nature of the SMS message by setting an appropriate PID value. The 59 | * value is masked to ensure it fits the allowed range defined by the protocol. 60 | * 61 | * @param pid The new PID value, which is masked to fit into the allowed range 62 | * @returns The instance of this PID, allowing for method chaining 63 | */ 64 | setPid(pid: number) { 65 | this._pid = 0x03 & pid; 66 | return this; 67 | } 68 | 69 | /** 70 | * Retrieves the indicates value. 71 | * 72 | * This value is part of determining how the message should be handled or processed by the 73 | * receiving entity, contributing to the overall interpretation of the PID. 74 | * 75 | * @returns The current indicates value 76 | */ 77 | get indicates() { 78 | return this._indicates; 79 | } 80 | 81 | /** 82 | * Sets the indicates value. 83 | * 84 | * Modifies part of the PID to influence how the SMS message is processed by the network or 85 | * receiving device. The value is masked to ensure it adheres to the expected range. 86 | * 87 | * @param indicates The new indicates value, which is masked to the allowed range 88 | * @returns The instance of this PID, allowing for method chaining 89 | */ 90 | setIndicates(indicates: number) { 91 | this._indicates = 0x01 & indicates; 92 | return this; 93 | } 94 | 95 | /** 96 | * Retrieves the type of the SMS message. 97 | * 98 | * The type provides further specification within the PID, defining the exact nature or 99 | * handling instructions for the message, such as whether it's a voice message or an email. 100 | * 101 | * @returns The current type value 102 | */ 103 | get type() { 104 | return this._type; 105 | } 106 | 107 | /** 108 | * Sets the type of the SMS message. 109 | * 110 | * This method allows for detailed specification of the message's nature, affecting how it is 111 | * processed. The type is masked to ensure it complies with the defined protocol specifications. 112 | * 113 | * @param type The new type value, masked to fit the allowed protocol range 114 | * @returns The instance of this PID, allowing for method chaining 115 | */ 116 | setType(type: number) { 117 | this._type = 0x1f & type; 118 | return this; 119 | } 120 | 121 | /* 122 | * ================================================ 123 | * Public functions 124 | * ================================================ 125 | */ 126 | 127 | /** 128 | * Computes and returns the combined value of the PID, including its indicates and type parts. 129 | * 130 | * This value is used for encoding the PID field in the SMS message, combining the general PID 131 | * with specific flags for message handling. 132 | * 133 | * @returns The combined PID value as a number 134 | */ 135 | getValue() { 136 | return (this._pid << 6) | (this._indicates << 5) | this._type; 137 | } 138 | 139 | /** 140 | * Provides a string representation of the PID's combined value. 141 | * 142 | * Useful for logging or debugging, this method returns a string that represents the encoded 143 | * value of the PID, including its general identifier, indicates, and type components. 144 | * 145 | * @returns The PID's combined value as a string 146 | */ 147 | toString() { 148 | return this.getValue().toString(); 149 | } 150 | } 151 | 152 | export type PIDOptions = { 153 | pid?: number; 154 | indicates?: number; 155 | type?: number; 156 | }; 157 | -------------------------------------------------------------------------------- /src/utils/PDU.ts: -------------------------------------------------------------------------------- 1 | import { DCS } from './DCS'; 2 | import { PID } from './PID'; 3 | import { SCA } from './SCA/SCA'; 4 | import type { DeliverType } from './Type/DeliverType'; 5 | import type { ReportType } from './Type/ReportType'; 6 | import type { SubmitType } from './Type/SubmitType'; 7 | 8 | /** 9 | * An abstract base class for Protocol Data Units (PDU) in SMS messaging. 10 | * 11 | * Defines the basic structure and functionalities common to all types of PDUs used in the GSM SMS system. 12 | * This class serves as a foundation for more specific PDU classes like Submit, Deliver, and Report. 13 | */ 14 | export abstract class PDU { 15 | abstract type: DeliverType | ReportType | SubmitType; 16 | 17 | private _address: SCA; 18 | private _serviceCenterAddress: SCA; 19 | private _protocolIdentifier: PID; 20 | private _dataCodingScheme: DCS; 21 | 22 | /** 23 | * Constructs a Protocol Description Unit (PDU) instance. 24 | * 25 | * @param address The address as a string or SCA instance 26 | * @param options An object containing optional parameters for the PDU instance 27 | */ 28 | constructor(address: string | SCA, options: PDUOptions = {}) { 29 | this._address = this.findAddress(address); 30 | 31 | this._serviceCenterAddress = options.serviceCenterAddress || new SCA(false); 32 | this._protocolIdentifier = options.protocolIdentifier || new PID(); 33 | this._dataCodingScheme = options.dataCodingScheme || new DCS(); 34 | } 35 | 36 | /* 37 | * ================================================ 38 | * Getter & Setter 39 | * ================================================ 40 | */ 41 | 42 | /** 43 | * Represents the address of the recipient. 44 | * @returns The current address as an SCA instance 45 | */ 46 | get address() { 47 | return this._address; 48 | } 49 | 50 | /** 51 | * Sets the address of the recipient. 52 | * 53 | * This method updates the address for the PDU. It accepts either a string or an SCA instance. 54 | * If a string is provided, it converts it to an SCA instance. 55 | * 56 | * @param address The new address, either as a string or an SCA instance 57 | * @returns The instance of this PDU, allowing for method chaining 58 | */ 59 | setAddress(address: string | SCA) { 60 | this._address = this.findAddress(address); 61 | return this; 62 | } 63 | 64 | /** 65 | * Returns the Service Center Address (SCA) of the SMS message. 66 | * 67 | * The Service Center Address is used by the GSM network to know where to send the SMS. 68 | * 69 | * @returns The current service center address as an SCA instance 70 | */ 71 | get serviceCenterAddress() { 72 | return this._serviceCenterAddress; 73 | } 74 | 75 | /** 76 | * Sets the Service Center Address (SCA) for the SMS message. 77 | * 78 | * This address is crucial for routing the message through the GSM network. The method accepts 79 | * either an SCA instance directly or a string that will be converted into an SCA. 80 | * 81 | * @param address The new service center address, either as a string or an SCA instance 82 | * @returns The instance of this PDU, allowing for method chaining 83 | */ 84 | setServiceCenterAddress(address: SCA | string) { 85 | if (address instanceof SCA) { 86 | this._serviceCenterAddress = address; 87 | return this; 88 | } 89 | 90 | this._serviceCenterAddress.setPhone(address, false, true); 91 | return this; 92 | } 93 | 94 | /** 95 | * Retrieves the Protocol Identifier (PID) of the SMS message. 96 | * 97 | * The PID is used to indicate interworking and teleservices. It determines how the message 98 | * should be processed by the receiving entity. 99 | * 100 | * @returns The current protocol identifier as a PID instance 101 | */ 102 | get protocolIdentifier() { 103 | return this._protocolIdentifier; 104 | } 105 | 106 | /** 107 | * Sets the Protocol Identifier (PID) for the SMS message. 108 | * 109 | * This method allows customization of the message's PID, which can affect delivery and processing. 110 | * Only PID instances are accepted to ensure correct format and compatibility. 111 | * 112 | * @param pid The new protocol identifier as a PID instance 113 | * @returns The instance of this PDU, allowing for method chaining 114 | */ 115 | setProtocolIdentifier(pid: PID) { 116 | this._protocolIdentifier = pid; 117 | return this; 118 | } 119 | 120 | /** 121 | * Retrieves the Data Coding Scheme (DCS) of the SMS message. 122 | * 123 | * The DCS indicates the data type of the message content (e.g., text, UCS2, etc.) and may 124 | * influence how the message is displayed on the recipient's device. 125 | * 126 | * @returns The current data coding scheme as a DCS instance 127 | */ 128 | get dataCodingScheme() { 129 | return this._dataCodingScheme; 130 | } 131 | 132 | /** 133 | * Sets the Data Coding Scheme (DCS) for the SMS message. 134 | * 135 | * Adjusting the DCS can change how the message content is interpreted and displayed by the 136 | * recipient's device. This method accepts only DCS instances to ensure proper format. 137 | * 138 | * @param dcs The new data coding scheme as a DCS instance 139 | * @returns The instance of this PDU, allowing for method chaining 140 | */ 141 | setDataCodingScheme(dcs: DCS) { 142 | this._dataCodingScheme = dcs; 143 | return this; 144 | } 145 | 146 | /* 147 | * ================================================ 148 | * Private functions 149 | * ================================================ 150 | */ 151 | 152 | private findAddress(address: string | SCA) { 153 | if (address instanceof SCA) { 154 | return address; 155 | } 156 | 157 | return new SCA().setPhone(address); 158 | } 159 | } 160 | 161 | export type PDUOptions = { 162 | serviceCenterAddress?: SCA; 163 | protocolIdentifier?: PID; 164 | dataCodingScheme?: DCS; 165 | }; 166 | -------------------------------------------------------------------------------- /src/parse/index.ts: -------------------------------------------------------------------------------- 1 | import { Deliver } from '../Deliver'; 2 | import { Report } from '../Report'; 3 | import { Submit } from '../Submit'; 4 | import { Helper } from '../utils/Helper'; 5 | import type { SCA } from '../utils/SCA/SCA'; 6 | import { DeliverType } from '../utils/Type/DeliverType'; 7 | import { ReportType } from '../utils/Type/ReportType'; 8 | import { SubmitType } from '../utils/Type/SubmitType'; 9 | 10 | // import the parser for the utils 11 | 12 | import parseData from './parseUtils/parseData'; 13 | import parseDCS from './parseUtils/parseDCS'; 14 | import parsePID from './parseUtils/parsePID'; 15 | import parseSCA from './parseUtils/parseSCA'; 16 | import parseSCTS from './parseUtils/parseSCTS'; 17 | import parseType from './parseUtils/parseType'; 18 | import parseVP from './parseUtils/parseVP'; 19 | 20 | export type GetSubstr = (length: number) => string; 21 | 22 | /** 23 | * Parses a PDU string into an instance of a PDU object (Deliver, Submit, or Report). 24 | * 25 | * This function is a high-level entry point for parsing PDU strings into their corresponding PDU object types 26 | * based on the detected PDU type within the provided string. It handles the initial parsing steps common to all PDU types, 27 | * such as the Service Center Address (SCA) and PDU type determination, and then delegates to specific parsing functions 28 | * based on the PDU type. 29 | * 30 | * @param str The PDU string to be parsed 31 | * 32 | * @returns An instance of a PDU object (Deliver, Submit, or Report) depending on the PDU type detected in the input string 33 | * @throws Throws an error if an unknown SMS type is encountered 34 | */ 35 | export function parse(str: string) { 36 | let pduParse = str.toUpperCase(); 37 | 38 | const getSubstr: GetSubstr = (length: number) => { 39 | const str = pduParse.substring(0, length); 40 | pduParse = pduParse.substring(length); 41 | 42 | return str; 43 | }; 44 | 45 | // The correct order of parsing is important!!! 46 | 47 | const sca = parseSCA(getSubstr, false); 48 | const type = parseType(getSubstr); 49 | 50 | if (type instanceof DeliverType) { 51 | return parseDeliver(sca, type, getSubstr); 52 | } 53 | 54 | if (type instanceof ReportType) { 55 | return parseReport(sca, type, getSubstr); 56 | } 57 | 58 | if (type instanceof SubmitType) { 59 | return parseSubmit(sca, type, getSubstr); 60 | } 61 | 62 | throw new Error('node-pdu: Unknown SMS type!'); 63 | } 64 | 65 | /** 66 | * Parses the "Deliver" type PDU from a given substring extractor. 67 | * This function extracts and constructs a Deliver PDU object from the provided PDU string parts. 68 | * 69 | * @param serviceCenterAddress The service center address extracted from the PDU string 70 | * @param type The detected DeliverType instance 71 | * @param getSubstr A function to extract substrings from the PDU string 72 | * 73 | * @returns An instance of Deliver containing parsed data 74 | */ 75 | function parseDeliver(serviceCenterAddress: SCA, type: DeliverType, getSubstr: GetSubstr) { 76 | // The correct order of parsing is important! 77 | 78 | const address = parseSCA(getSubstr, true); 79 | const protocolIdentifier = parsePID(getSubstr); 80 | const dataCodingScheme = parseDCS(getSubstr); 81 | const serviceCenterTimeStamp = parseSCTS(getSubstr); 82 | const userDataLength = Helper.getByteFromHex(getSubstr(2)); 83 | const userData = parseData(type, dataCodingScheme, userDataLength, getSubstr); 84 | 85 | return new Deliver(address, userData, { serviceCenterAddress, type, protocolIdentifier, dataCodingScheme, serviceCenterTimeStamp }); 86 | } 87 | 88 | /** 89 | * Parses the "Report" type PDU from a given substring extractor. 90 | * This function extracts and constructs a Report PDU object from the provided PDU string parts. 91 | * 92 | * @param serviceCenterAddress The service center address extracted from the PDU string 93 | * @param type The detected ReportType instance 94 | * @param getSubstr A function to extract substrings from the PDU string 95 | * 96 | * @returns An instance of Report containing parsed data 97 | */ 98 | function parseReport(serviceCenterAddress: SCA, type: ReportType, getSubstr: GetSubstr) { 99 | // The correct order of parsing is important! 100 | 101 | const referencedBytes = Helper.getByteFromHex(getSubstr(2)); 102 | const address = parseSCA(getSubstr, true); 103 | const timestamp = parseSCTS(getSubstr); 104 | const discharge = parseSCTS(getSubstr); 105 | const status = Helper.getByteFromHex(getSubstr(2)); 106 | 107 | return new Report(address, referencedBytes, timestamp, discharge, status, { serviceCenterAddress, type }); 108 | } 109 | 110 | /** 111 | * Parses the "Submit" type PDU from a given substring extractor. 112 | * This function extracts and constructs a Submit PDU object from the provided PDU string parts. 113 | * 114 | * @param serviceCenterAddress The service center address extracted from the PDU string 115 | * @param type The detected SubmitType instance 116 | * @param getSubstr A function to extract substrings from the PDU string 117 | * 118 | * @returns An instance of Submit containing parsed data 119 | */ 120 | function parseSubmit(serviceCenterAddress: SCA, type: SubmitType, getSubstr: GetSubstr) { 121 | // The correct order of parsing is important! 122 | 123 | const messageReference = Helper.getByteFromHex(getSubstr(2)); 124 | const address = parseSCA(getSubstr, true); 125 | const protocolIdentifier = parsePID(getSubstr); 126 | const dataCodingScheme = parseDCS(getSubstr); 127 | const validityPeriod = parseVP(type, getSubstr); 128 | const userDataLength = Helper.getByteFromHex(getSubstr(2)); 129 | const userData = parseData(type, dataCodingScheme, userDataLength, getSubstr); 130 | 131 | return new Submit(address, userData, { 132 | serviceCenterAddress, 133 | type, 134 | messageReference, 135 | protocolIdentifier, 136 | dataCodingScheme, 137 | validityPeriod 138 | }); 139 | } 140 | -------------------------------------------------------------------------------- /src/utils/Type/PDUType.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from '../Helper'; 2 | 3 | /** 4 | * Represents the abstract base for different types of Protocol Data Units (PDU) in SMS messaging. 5 | * 6 | * This abstract class defines the core structure for various SMS PDU types and 7 | * ensuring standardized handling and processing across different messaging operations. 8 | */ 9 | export abstract class PDUType { 10 | abstract messageTypeIndicator: number; 11 | 12 | static readonly SMS_SUBMIT = 0x01; 13 | static readonly SMS_DELIVER = 0x00; 14 | static readonly SMS_REPORT = 0x02; 15 | 16 | static readonly VPF_NONE = 0x00; 17 | static readonly VPF_SIEMENS = 0x01; 18 | static readonly VPF_RELATIVE = 0x02; 19 | static readonly VPF_ABSOLUTE = 0x03; 20 | 21 | private readonly replyPath: number; 22 | private readonly rejectDuplicates: number; 23 | private _userDataHeader: number; 24 | private _statusReportRequest: number; 25 | private _validityPeriodFormat: number; 26 | 27 | /** 28 | * Constructs a PDUType instance. 29 | * @param params Parameters for configuring the PDUType instance 30 | */ 31 | constructor(params: TypeParams) { 32 | this.replyPath = params.replyPath; 33 | this._userDataHeader = params.userDataHeader; 34 | this._statusReportRequest = params.statusReportRequest; 35 | this._validityPeriodFormat = params.validityPeriodFormat; 36 | this.rejectDuplicates = params.rejectDuplicates; 37 | } 38 | 39 | /* 40 | * ================================================ 41 | * Getter & Setter 42 | * ================================================ 43 | */ 44 | 45 | /** 46 | * Retrieves the User Data Header (UDH) indicator status. 47 | * 48 | * The User Data Header is part of the SMS payload and can contain various control 49 | * information such as concatenated message reference numbers, port numbers for WAP 50 | * Push, and other service indications. This indicator specifies whether UDH is present. 51 | * 52 | * @returns The current status of the User Data Header indicator 53 | */ 54 | get userDataHeader() { 55 | return this._userDataHeader; 56 | } 57 | 58 | /** 59 | * Sets the User Data Header (UDH) indicator. 60 | * 61 | * This method configures the presence of a User Data Header in the SMS message. It is 62 | * primarily used for advanced messaging features such as concatenated SMS or application 63 | * port addressing. The value is masked to ensure it is within valid range. 64 | * 65 | * @param userDataHeader The desired status for the UDH indicator (0 or 1) 66 | * @returns The instance of this PDUType, allowing for method chaining 67 | */ 68 | setUserDataHeader(userDataHeader: number) { 69 | this._userDataHeader = 0x01 & userDataHeader; 70 | return this; 71 | } 72 | 73 | /** 74 | * Retrieves the Status Report Request (SRR) status. 75 | * 76 | * The SRR is a feature in SMS that requests the network to send a delivery report 77 | * for the sent message. This indicator specifies whether such a report is requested. 78 | * 79 | * @returns The current status of the Status Report Request indicator 80 | */ 81 | get statusReportRequest() { 82 | return this._statusReportRequest; 83 | } 84 | 85 | /** 86 | * Sets the Status Report Request (SRR) indicator. 87 | * 88 | * This method enables or disables the request for a delivery report for the SMS message. 89 | * It ensures the delivery status can be tracked. The value is masked to ensure it is 90 | * within valid range. 91 | * 92 | * @param srr The desired status for the SRR indicator (0 or 1) 93 | * @returns The instance of this PDUType, allowing for method chaining 94 | */ 95 | setStatusReportRequest(srr: number) { 96 | this._statusReportRequest = 0x01 & srr; 97 | return this; 98 | } 99 | 100 | /** 101 | * Retrieves the Validity Period Format (VPF). 102 | * 103 | * The VPF specifies the format of the validity period of the SMS message, dictating how 104 | * long the message should be stored in the network before delivery is attempted. It supports 105 | * several formats including none, relative, and absolute. 106 | * 107 | * @returns The current format of the validity period 108 | */ 109 | get validityPeriodFormat() { 110 | return this._validityPeriodFormat; 111 | } 112 | 113 | /** 114 | * Sets the Validity Period Format (VPF) for the SMS message. 115 | * 116 | * This method configures the time frame in which the SMS should be delivered. It is crucial 117 | * for time-sensitive messages. The value is masked and validated to ensure it corresponds to 118 | * one of the predefined formats. An error is thrown for invalid formats. 119 | * 120 | * @param validityPeriodFormat The desired format for the message validity period 121 | * @returns The instance of this PDUType, allowing for method chaining 122 | */ 123 | setValidityPeriodFormat(validityPeriodFormat: number) { 124 | this._validityPeriodFormat = 0x03 & validityPeriodFormat; 125 | 126 | switch (this._validityPeriodFormat) { 127 | case PDUType.VPF_NONE: 128 | break; 129 | case PDUType.VPF_SIEMENS: 130 | break; 131 | case PDUType.VPF_RELATIVE: 132 | break; 133 | case PDUType.VPF_ABSOLUTE: 134 | break; 135 | default: 136 | throw new Error('node-pdu: Wrong validity period format!'); 137 | } 138 | 139 | return this; 140 | } 141 | 142 | /* 143 | * ================================================ 144 | * Public functions 145 | * ================================================ 146 | */ 147 | 148 | /** 149 | * Calculates and returns the overall value of the PDU type based on its components. 150 | * 151 | * This method aggregates the various indicators and settings into a single value, which 152 | * represents the configuration of the PDU type. It is used for generating the final PDU 153 | * string representation. 154 | * 155 | * @returns The aggregated value of the PDU type settings 156 | */ 157 | getValue() { 158 | return ( 159 | ((1 & this.replyPath) << 7) | 160 | ((1 & this._userDataHeader) << 6) | 161 | ((1 & this._statusReportRequest) << 5) | 162 | ((3 & this._validityPeriodFormat) << 3) | 163 | ((1 & this.rejectDuplicates) << 2) | 164 | (3 & this.messageTypeIndicator) 165 | ); 166 | } 167 | 168 | /** 169 | * Generates a string representation of the PDU type value. 170 | * 171 | * This method utilizes a helper function to convert the aggregated PDU type value into 172 | * a hexadecimal string. It is useful for logging and debugging purposes to see the 173 | * encoded PDU type. 174 | * 175 | * @returns A hexadecimal string representation of the PDU type value 176 | */ 177 | toString() { 178 | return Helper.toStringHex(this.getValue()); 179 | } 180 | } 181 | 182 | export type TypeParams = { 183 | replyPath: number; 184 | userDataHeader: number; 185 | statusReportRequest: number; 186 | validityPeriodFormat: number; 187 | rejectDuplicates: number; 188 | }; 189 | -------------------------------------------------------------------------------- /src/utils/SCA/SCA.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from '../Helper'; 2 | import { SCAType } from './SCAType'; 3 | 4 | /** 5 | * Represents the Service Centre Address (SCA) of an SMS message. 6 | * 7 | * The address of the SMSC responsible for the delivery of the message. It is crucial for routing the SMS 8 | * through the correct service center to reach the intended recipient. 9 | */ 10 | export class SCA { 11 | type: SCAType; 12 | 13 | private _isAddress: boolean; 14 | private _size = 0x00; 15 | private _encoded = ''; 16 | private _phone: string | null = null; 17 | 18 | constructor(isAddress = false, options: SCAOptions = {}) { 19 | this.type = options.type || new SCAType(); 20 | this._isAddress = isAddress; 21 | } 22 | 23 | /* 24 | * ================================================ 25 | * Getter & Setter 26 | * ================================================ 27 | */ 28 | 29 | /** 30 | * Indicates whether the current instance represents an address. 31 | * @returns True if the instance represents an address; otherwise, false 32 | */ 33 | get isAddress() { 34 | return this._isAddress; 35 | } 36 | 37 | /** 38 | * Retrieves the size of the encoded address. 39 | * The size is calculated based on the encoding scheme used. 40 | * 41 | * @returns The size of the encoded address 42 | */ 43 | get size() { 44 | return this._size; 45 | } 46 | 47 | /** 48 | * Retrieves the encoded representation of the address. 49 | * The encoding scheme depends on the type of address. 50 | * 51 | * @returns The encoded address as a hexadecimal string 52 | */ 53 | get encoded() { 54 | return this._encoded; 55 | } 56 | 57 | /** 58 | * Retrieves the phone number associated with the address. 59 | * @returns The phone number associated with the address 60 | */ 61 | get phone() { 62 | return this._phone; 63 | } 64 | 65 | /** 66 | * Sets the phone number associated with the address. 67 | * 68 | * This method allows setting the phone number for the address. It also performs 69 | * encoding based on the address type and detects the type of address if requested. 70 | * 71 | * @param phone The phone number to set 72 | * @param detectType Flag indicating whether to detect the address type 73 | * @param SC Flag indicating whether the address is a service center address 74 | * 75 | * @returns The instance of this SCA, allowing for method chaining 76 | */ 77 | setPhone(phone: string, detectType = true, SC = false) { 78 | this._phone = phone.trim(); 79 | this._isAddress = !SC; 80 | 81 | if (this._isAddress && detectType) { 82 | this.detectScaType(this._phone); 83 | } 84 | 85 | if (this.type.type === SCAType.TYPE_ALPHANUMERICAL) { 86 | const tmp = Helper.encode7Bit(phone); 87 | 88 | this._size = Math.ceil((tmp.length * 7) / 4); // septets to semi-octets 89 | this._encoded = tmp.result; 90 | 91 | return this; 92 | } 93 | 94 | const clear = this._phone.replace(/[^a-c0-9*#]/gi, ''); 95 | 96 | // get size 97 | // service center address counting by octets OA or DA as length numbers 98 | this._size = SC ? 1 + Math.ceil(clear.length / 2) : clear.length; 99 | 100 | this._encoded = clear 101 | .split('') 102 | .map((s) => SCA.mapFilterEncode(s)) 103 | .join(''); 104 | 105 | return this; 106 | } 107 | 108 | /* 109 | * ================================================ 110 | * Private functions 111 | * ================================================ 112 | */ 113 | 114 | private detectScaType(phone: string) { 115 | const phoneSpaceless = phone.replace(/^\s+|\s+$/g, ''); 116 | 117 | if (/\+\d+$/.test(phoneSpaceless)) { 118 | this._phone = phoneSpaceless.substring(1); 119 | this.type.setType(SCAType.TYPE_INTERNATIONAL); 120 | return; 121 | } 122 | 123 | if (/00\d+$/.test(phoneSpaceless)) { 124 | this._phone = phoneSpaceless.substring(2); 125 | this.type.setType(SCAType.TYPE_INTERNATIONAL); 126 | return; 127 | } 128 | 129 | if (/\d+$/.test(phoneSpaceless)) { 130 | this._phone = phoneSpaceless; 131 | this.type.setType(SCAType.TYPE_UNKNOWN); 132 | return; 133 | } 134 | 135 | this.type.setType(SCAType.TYPE_ALPHANUMERICAL); 136 | } 137 | 138 | /* 139 | * ================================================ 140 | * Public functions 141 | * ================================================ 142 | */ 143 | 144 | /** 145 | * Retrieves the offset for the SCA in the PDU. 146 | * The offset indicates the position of the SCA within the PDU. 147 | * 148 | * @returns The offset for the SCA in the PDU 149 | */ 150 | getOffset() { 151 | return !this._size ? 2 : this._size + 4; 152 | } 153 | 154 | /** 155 | * Converts the SCA instance to a string representation. 156 | * 157 | * This method converts the SCA instance to its string representation suitable for 158 | * inclusion in the PDU. 159 | * 160 | * @returns The string representation of the SCA 161 | */ 162 | toString() { 163 | let str = Helper.toStringHex(this.size); 164 | 165 | if (this.size !== 0) { 166 | str += this.type.toString(); 167 | 168 | if (this.type.type !== SCAType.TYPE_ALPHANUMERICAL) { 169 | // reverse octets 170 | const l = this.encoded.length; 171 | 172 | for (let i = 0; i < l; i += 2) { 173 | const b1 = this.encoded.substring(i, i + 1); 174 | const b2 = i + 1 >= l ? 'F' : this.encoded.substring(i + 1, i + 2); 175 | 176 | // add to pdu 177 | str += b2 + b1; 178 | } 179 | } else { 180 | str += this.encoded; 181 | } 182 | } 183 | 184 | return str; 185 | } 186 | 187 | /* 188 | * ================================================ 189 | * Static functions 190 | * ================================================ 191 | */ 192 | 193 | /** 194 | * Maps and decodes a hexadecimal letter to its corresponding character. 195 | * This method decodes a hexadecimal letter back to its original character value. 196 | * 197 | * @param letter The hexadecimal letter to decode 198 | * @returns The decoded character 199 | */ 200 | static mapFilterDecode(letter: string) { 201 | const byte = Helper.getByteFromHex(letter); 202 | 203 | switch (byte) { 204 | case 0x0a: 205 | return '*'; 206 | case 0x0b: 207 | return '#'; 208 | case 0x0c: 209 | return 'a'; 210 | case 0x0d: 211 | return 'b'; 212 | case 0x0e: 213 | return 'c'; 214 | default: 215 | return letter; 216 | } 217 | } 218 | 219 | /** 220 | * Maps and encodes a character to its corresponding hexadecimal representation. 221 | * This method encodes a character to its hexadecimal representation. 222 | * 223 | * @param letter The character to encode 224 | * @returns The encoded hexadecimal representation 225 | */ 226 | static mapFilterEncode(letter: string) { 227 | switch (letter) { 228 | case '*': 229 | return 'A'; 230 | case '#': 231 | return 'B'; 232 | case 'a': 233 | return 'C'; 234 | case 'b': 235 | return 'D'; 236 | case 'c': 237 | return 'E'; 238 | default: 239 | return letter; 240 | } 241 | } 242 | } 243 | 244 | export type SCAOptions = { 245 | type?: SCAType; 246 | }; 247 | -------------------------------------------------------------------------------- /src/Deliver.ts: -------------------------------------------------------------------------------- 1 | import { Data } from './utils/Data/Data'; 2 | import { Helper } from './utils/Helper'; 3 | import { PDU, type PDUOptions } from './utils/PDU'; 4 | import type { SCA } from './utils/SCA/SCA'; 5 | import { SCTS } from './utils/SCTS'; 6 | import { DeliverType } from './utils/Type/DeliverType'; 7 | 8 | /** 9 | * Represents a GSM SMS Deliver PDU. 10 | * 11 | * Used for receiving SMS messages. It includes information like the sender's address and the message content, 12 | * essential for any application or service that needs to process or display incoming text messages to users. 13 | */ 14 | export class Deliver extends PDU { 15 | private _type: DeliverType; 16 | private _data: Data; 17 | private _serviceCenterTimeStamp: SCTS; 18 | 19 | /** 20 | * Constructs a SMS Deliver PDU instance. 21 | * 22 | * @param address The sender's address as a string or an instance of SCA 23 | * @param data The message content as a string or an instance of Data 24 | * @param options An object containing optional parameters for the Deliver instance 25 | */ 26 | constructor(address: string | SCA, data: string | Data, options: DeliverOptions = {}) { 27 | super(address, options); 28 | 29 | this._type = options.type || new DeliverType(); 30 | this._data = this.findData(data); 31 | this._serviceCenterTimeStamp = options.serviceCenterTimeStamp || new SCTS(this.getDateTime()); 32 | } 33 | 34 | /* 35 | * ================================================ 36 | * Getter & Setter 37 | * ================================================ 38 | */ 39 | 40 | /** 41 | * Retrieves the message type for this Deliver PDU. 42 | * 43 | * This property represents the specific characteristics and parameters related to the delivery of the SMS message, 44 | * such as whether it's a standard message, a status report request, etc. 45 | * 46 | * @returns The current DeliverType instance, defining the message type 47 | */ 48 | get type() { 49 | return this._type; 50 | } 51 | 52 | /** 53 | * Sets the message type for this Deliver PDU. 54 | * 55 | * Allows for specifying the type of SMS deliver message, adjusting its parameters like whether it requests 56 | * a status report, among other options. 57 | * 58 | * @param type The new DeliverType instance to set as the message type 59 | * @returns The instance of this Deliver, allowing for method chaining 60 | */ 61 | setType(type: DeliverType) { 62 | this._type = type; 63 | return this; 64 | } 65 | 66 | /** 67 | * Retrieves the data/content of the SMS message. 68 | * 69 | * This property holds the actual message content that was sent, which can be in various formats 70 | * (e.g., plain text, binary data) depending on the data coding scheme. 71 | * 72 | * @returns The current Data instance containing the message content 73 | */ 74 | get data() { 75 | return this._data; 76 | } 77 | 78 | /** 79 | * Sets the data/content of the SMS message. 80 | * 81 | * This method allows the message content to be updated, accepting either a string (which will be 82 | * converted to a Data instance internally) or a Data instance directly. 83 | * 84 | * @param data The new message content, either as a string or a Data instance 85 | * @returns The instance of this Deliver, allowing for method chaining 86 | */ 87 | setData(data: string | Data) { 88 | this._data = this.findData(data); 89 | return this; 90 | } 91 | 92 | /** 93 | * Retrieves the timestamp from the service center. 94 | * 95 | * This timestamp represents when the SMS message was received by the service center, providing 96 | * information about the message's delivery time. 97 | * 98 | * @returns The current SCTS (Service Center Time Stamp) instance 99 | */ 100 | get serviceCenterTimeStamp() { 101 | return this._serviceCenterTimeStamp; 102 | } 103 | 104 | /** 105 | * Sets the service center timestamp for the SMS message. 106 | * 107 | * This method updates the timestamp indicating when the message was received by the service center. 108 | * It accepts either a Date object or an SCTS instance. If a Date is provided, it is converted to an SCTS. 109 | * 110 | * @param time The new timestamp, either as a Date or an SCTS instance 111 | * @returns The instance of this Deliver, allowing for method chaining 112 | */ 113 | setServiceCenterTimeStamp(time: Date | SCTS = this.getDateTime()) { 114 | if (time instanceof SCTS) { 115 | this._serviceCenterTimeStamp = time; 116 | return this; 117 | } 118 | 119 | this._serviceCenterTimeStamp = new SCTS(time); 120 | 121 | return this; 122 | } 123 | 124 | /* 125 | * ================================================ 126 | * Private functions 127 | * ================================================ 128 | */ 129 | 130 | private getDateTime() { 131 | // Create Date in the increment of 10 days 132 | return new Date(Date.now() + 864000000); 133 | } 134 | 135 | private findData(data: string | Data) { 136 | if (data instanceof Data) { 137 | return data; 138 | } 139 | 140 | return new Data().setData(data, this); 141 | } 142 | 143 | /* 144 | * ================================================ 145 | * Public functions 146 | * ================================================ 147 | */ 148 | 149 | /** 150 | * Retrieves all parts of the message data. 151 | * 152 | * For messages that are split into multiple parts (e.g., concatenated SMS), this method returns 153 | * all parts as an array. 154 | * 155 | * @returns An array of Data parts representing the message content 156 | */ 157 | getParts() { 158 | return this._data.parts; 159 | } 160 | 161 | /** 162 | * Retrieves all parts of the message as strings. 163 | * 164 | * This method is useful for concatenated messages, converting each part of the message data 165 | * into a string format based on the current data coding scheme. 166 | * 167 | * @returns An array of strings, each representing a part of the message content 168 | */ 169 | getPartStrings() { 170 | return this._data.parts.map((part) => part.toString(this)); 171 | } 172 | 173 | /** 174 | * Generates a string representation of the start of the PDU. 175 | * 176 | * This method constructs the initial part of the PDU string, including information like the 177 | * service center address, message type, sender's address, protocol identifier, data coding scheme, 178 | * and service center timestamp. 179 | * 180 | * @returns A string representing the start of the PDU 181 | */ 182 | getStart() { 183 | let str = ''; 184 | 185 | str += this.serviceCenterAddress.toString(); 186 | str += this._type.toString(); 187 | str += this.address.toString(); 188 | str += Helper.toStringHex(this.protocolIdentifier.getValue()); 189 | str += this.dataCodingScheme.toString(); 190 | str += this._serviceCenterTimeStamp.toString(); 191 | 192 | return str; 193 | } 194 | 195 | /** 196 | * Converts the entire Deliver PDU into a string representation. 197 | * 198 | * This method is intended to provide a complete textual representation of the Deliver PDU, 199 | * including all headers and the message content, formatted according to the PDU protocol. 200 | * 201 | * @returns A string representation of the Deliver PDU 202 | */ 203 | toString() { 204 | return this.getStart(); 205 | } 206 | } 207 | 208 | export type DeliverOptions = PDUOptions & { 209 | type?: DeliverType; 210 | serviceCenterTimeStamp?: SCTS; 211 | }; 212 | -------------------------------------------------------------------------------- /src/utils/Data/Header.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from '../Helper'; 2 | 3 | /** 4 | * Represents the header information in a segmented SMS message part. 5 | * 6 | * Contains metadata essential for the reassembly of segmented SMS messages, such as part numbering and 7 | * reference identifiers. It ensures that multipart messages are correctly reconstructed upon receipt. 8 | */ 9 | export class Header { 10 | static readonly IE_CONCAT_8BIT_REF = 0x00; 11 | static readonly IE_CONCAT_16BIT_REF = 0x08; 12 | 13 | private ies: IES[] = []; 14 | private concatIeIdx?: number; 15 | 16 | /** 17 | * Constructs a Header instance. 18 | * @param params The parameters for constructing the Header instance 19 | */ 20 | constructor(params: HeaderParams) { 21 | if (Array.isArray(params)) { 22 | /* 23 | * NB: This code can be factored out into a separate method if we have 24 | * a usecase for it. 25 | */ 26 | 27 | for (const ie of params) { 28 | const buf = Helper.hexToUint8Array(ie.dataHex); 29 | 30 | // Parse known IEs (e.g. concatenetion) 31 | if (ie.type === Header.IE_CONCAT_8BIT_REF) { 32 | // Preserve IE index 33 | this.concatIeIdx = this.ies.length; 34 | 35 | this.ies.push({ 36 | type: ie.type, 37 | dataHex: ie.dataHex, 38 | data: { 39 | msgRef: buf[0], 40 | maxMsgNum: buf[1], 41 | msgSeqNo: buf[2] 42 | } 43 | }); 44 | 45 | continue; 46 | } 47 | 48 | if (ie.type === Header.IE_CONCAT_16BIT_REF) { 49 | // Preserve IE index 50 | this.concatIeIdx = this.ies.length; 51 | 52 | this.ies.push({ 53 | type: ie.type, 54 | dataHex: ie.dataHex, 55 | data: { 56 | msgRef: (buf[0] << 8) | buf[1], 57 | maxMsgNum: buf[2], 58 | msgSeqNo: buf[3] 59 | } 60 | }); 61 | 62 | continue; 63 | } 64 | 65 | this.ies.push({ 66 | type: ie.type, 67 | dataHex: ie.dataHex 68 | }); 69 | } 70 | 71 | return; 72 | } 73 | 74 | const dataHex = Helper.toStringHex(params.POINTER, 4) + Helper.toStringHex(params.SEGMENTS) + Helper.toStringHex(params.CURRENT); 75 | 76 | this.ies.push({ 77 | type: Header.IE_CONCAT_16BIT_REF, 78 | dataHex: dataHex, 79 | data: { 80 | msgRef: params.POINTER, 81 | maxMsgNum: params.SEGMENTS, 82 | msgSeqNo: params.CURRENT 83 | } 84 | }); 85 | 86 | this.concatIeIdx = this.ies.length - 1; 87 | } 88 | 89 | /* 90 | * ================================================ 91 | * Public functions 92 | * ================================================ 93 | */ 94 | 95 | /** 96 | * Converts the header information to an object. 97 | * 98 | * This function is useful for serializing the header information, including the message reference number, 99 | * total number of segments, and current segment number. It's particularly useful for diagnostics or 100 | * interfacing with systems that require these details in a structured format. 101 | * 102 | * @returns An object containing the pointer 103 | */ 104 | toJSON() { 105 | return { 106 | POINTER: this.getPointer(), 107 | SEGMENTS: this.getSegments(), 108 | CURRENT: this.getCurrent() 109 | }; 110 | } 111 | 112 | /** 113 | * Calculates the total size of the User Data Header Length (UDHL). 114 | * 115 | * The size is calculated based on the length of all Information Elements (IEs) included in the header. 116 | * This is crucial for correctly encoding and decoding segmented SMS messages. 117 | * 118 | * @returns The total size of the UDHL 119 | */ 120 | getSize() { 121 | let udhl = 0; 122 | 123 | this.ies.forEach((ie) => { 124 | udhl += 2 + ie.dataHex.length / 2; 125 | }); 126 | 127 | return udhl; 128 | } 129 | 130 | /** 131 | * Retrieves the type of the concatenation Information Element (IE). 132 | * 133 | * This method checks if there's a known concatenation IE present and returns its type. 134 | * It distinguishes between 8-bit and 16-bit reference numbers for concatenated messages. 135 | * 136 | * @returns The type of the concatenation IE, or undefined if not present 137 | */ 138 | getType() { 139 | if (this.concatIeIdx !== undefined) { 140 | return this.ies[this.concatIeIdx].type; 141 | } 142 | 143 | return; 144 | } 145 | 146 | /** 147 | * Gets the size of the pointer (message reference number) in bytes. 148 | * 149 | * The size is determined based on the type of concatenation IE present, reflecting the 150 | * length of the message reference number field. 151 | * 152 | * @returns The size of the pointer in bytes, or 0 if no concatenation IE is present 153 | */ 154 | getPointerSize() { 155 | if (this.concatIeIdx !== undefined) { 156 | return this.ies[this.concatIeIdx].dataHex.length / 2; 157 | } 158 | 159 | return 0; 160 | } 161 | 162 | /** 163 | * Retrieves the message reference number for the segmented SMS message. 164 | * 165 | * This number is used to correlate all segments of a multi-part SMS message, ensuring 166 | * they can be correctly reassembled upon receipt. 167 | * 168 | * @returns The message reference number, or 0 if no concatenation IE is present 169 | */ 170 | getPointer() { 171 | if (this.concatIeIdx !== undefined) { 172 | return this.ies[this.concatIeIdx].data?.msgRef; 173 | } 174 | 175 | return 0; 176 | } 177 | 178 | /** 179 | * Gets the total number of segments in the segmented SMS message. 180 | * 181 | * This information is critical for understanding how many parts the message has been 182 | * divided into, enabling correct reassembly. 183 | * 184 | * @returns The total number of segments, or 1 if no concatenation IE is present 185 | */ 186 | getSegments() { 187 | if (this.concatIeIdx !== undefined) { 188 | return this.ies[this.concatIeIdx].data?.maxMsgNum; 189 | } 190 | 191 | return 1; 192 | } 193 | 194 | /** 195 | * Retrieves the current segment number of this part of the segmented SMS message. 196 | * 197 | * This number indicates the order of the current segment in the sequence of the total 198 | * message parts, aiding in proper reconstruction. 199 | * 200 | * @returns The current segment number, or 1 if no concatenation IE is present 201 | */ 202 | getCurrent() { 203 | if (this.concatIeIdx !== undefined) { 204 | return this.ies[this.concatIeIdx].data?.msgSeqNo; 205 | } 206 | 207 | return 1; 208 | } 209 | 210 | /** 211 | * Generates a string representation of the User Data Header (UDH). 212 | * 213 | * This method constructs the UDH string by concatenating the encoded lengths and data of all 214 | * Information Elements (IEs), prefixed with the overall UDH length. It's essential for 215 | * encoding the header part of segmented SMS messages. 216 | * 217 | * @returns A string representing the encoded User Data Header 218 | */ 219 | toString() { 220 | let udhl = 0; 221 | let head = ''; 222 | 223 | this.ies.forEach((ie) => { 224 | udhl += 2 + ie.dataHex.length / 2; 225 | head += Helper.toStringHex(ie.type) + Helper.toStringHex(ie.dataHex.length / 2) + ie.dataHex; 226 | }); 227 | 228 | return Helper.toStringHex(udhl) + head; 229 | } 230 | } 231 | 232 | export type HeaderParams = 233 | | { 234 | POINTER: number; 235 | SEGMENTS: number; 236 | CURRENT: number; 237 | } 238 | | { 239 | type: number; 240 | dataHex: string; 241 | }[]; 242 | 243 | export type IES = { 244 | type: number; 245 | dataHex: string; 246 | data?: { 247 | msgRef: number; 248 | maxMsgNum: number; 249 | msgSeqNo: number; 250 | }; 251 | }; 252 | -------------------------------------------------------------------------------- /src/Report.ts: -------------------------------------------------------------------------------- 1 | import { PDU, type PDUOptions } from './utils/PDU'; 2 | import type { SCA } from './utils/SCA/SCA'; 3 | import type { SCTS } from './utils/SCTS'; 4 | import { ReportType } from './utils/Type/ReportType'; 5 | 6 | /** 7 | * Represents a GSM SMS delivery Report PDU. 8 | * 9 | * Provides feedback on the delivery status of sent SMS messages, indicating success, failure, or delay. 10 | * This is crucial for ensuring message reliability in services that require confirmation of message delivery, 11 | * such as transaction alerts and critical notifications. 12 | */ 13 | export class Report extends PDU { 14 | private _type: ReportType; 15 | private _reference: number; 16 | private _dateTime: SCTS; 17 | private _discharge: SCTS; 18 | 19 | /* 20 | * report status 21 | * 0x00 Short message received succesfully 22 | * 0x01 Short message forwarded to the mobile phone, but unable to confirm delivery 23 | * 0x02 Short message replaced by the service center 24 | * 0x20 Congestion 25 | * 0x21 SME busy 26 | * 0x22 No response from SME 27 | * 0x23 Service rejected 28 | * 0x24 Quality of service not available 29 | * 0x25 Error in SME 30 | * 0x40 Remote procedure error 31 | * 0x41 Incompatible destination 32 | * 0x42 Connection rejected by SME 33 | * 0x43 Not obtainable 34 | * 0x44 Quality of service not available 35 | * 0x45 No interworking available 36 | * 0x46 SM validity period expired 37 | * 0x47 SM deleted by originating SME 38 | * 0x48 SM deleted by service center administration 39 | * 0x49 SM does not exist 40 | * 0x60 Congestion 41 | * 0x61 SME busy 42 | * 0x62 No response from SME 43 | * 0x63 Service rejected 44 | * 0x64 Quality of service not available 45 | * 0x65 Error in SME 46 | */ 47 | private _status: number; 48 | 49 | /** 50 | * Constructs a Report PDU instance. 51 | * 52 | * @param address The sender's address as a string or an instance of SCA 53 | * @param reference The reference number of the SMS message for which this report is generated 54 | * @param dateTime The original message submission timestamp represented by an SCTS instance 55 | * @param discharge The discharge time indicating the completion of the delivery process, as an SCTS instance 56 | * @param status The status code indicating the outcome of the delivery attempt 57 | * @param options An object containing optional parameters for the Report instance 58 | */ 59 | constructor(address: string | SCA, reference: number, dateTime: SCTS, discharge: SCTS, status: number, options: ReportOptions = {}) { 60 | super(address, options); 61 | 62 | this._type = options.type || new ReportType(); 63 | this._reference = reference; 64 | this._dateTime = dateTime; 65 | this._discharge = discharge; 66 | this._status = status; 67 | } 68 | 69 | /* 70 | * ================================================ 71 | * Getter & Setter 72 | * ================================================ 73 | */ 74 | 75 | /** 76 | * Retrieves the message type for this Report PDU. 77 | * 78 | * This property indicates the specific type of delivery report, such as whether it's a delivery 79 | * acknowledgment or a failure report, providing context for the reported delivery status. 80 | * 81 | * @returns The current ReportType instance, defining the report type 82 | */ 83 | get type() { 84 | return this._type; 85 | } 86 | 87 | /** 88 | * Sets the message type for this Report PDU. 89 | * 90 | * Allows for specifying the type of delivery report, which impacts the interpretation of the 91 | * report's content and the delivery status it conveys. 92 | * 93 | * @param type The new ReportType instance to set as the report type 94 | * @returns The instance of this Report, allowing for method chaining 95 | */ 96 | setType(type: ReportType) { 97 | this._type = type; 98 | return this; 99 | } 100 | 101 | /** 102 | * Retrieves the message reference number. 103 | * 104 | * This number uniquely identifies the SMS message for which this delivery report is generated, 105 | * allowing for correlation between sent messages and their respective delivery reports. 106 | * 107 | * @returns The reference number of the SMS message 108 | */ 109 | get reference() { 110 | return this._reference; 111 | } 112 | 113 | /** 114 | * Sets the message reference number. 115 | * 116 | * Updates the reference number to match the SMS message this delivery report pertains to, 117 | * ensuring accurate identification and tracking of message delivery status. 118 | * 119 | * @param reference The new reference number for the SMS message 120 | * @returns The instance of this Report, allowing for method chaining 121 | */ 122 | setReference(reference: number) { 123 | this._reference = reference; 124 | return this; 125 | } 126 | 127 | /** 128 | * Retrieves the original message submission timestamp. 129 | * 130 | * This timestamp represents when the SMS message was originally submitted for delivery, 131 | * providing context for the delivery process and timing. 132 | * 133 | * @returns The SCTS (Service Center Time Stamp) instance representing the submission timestamp 134 | */ 135 | get dateTime() { 136 | return this._dateTime; 137 | } 138 | 139 | /** 140 | * Sets the original message submission timestamp. 141 | * 142 | * Updates the timestamp indicating when the SMS message was submitted, which is useful for 143 | * tracking the duration between message submission and its final delivery status. 144 | * 145 | * @param dateTime The new SCTS instance representing the submission timestamp 146 | * @returns The instance of this Report, allowing for method chaining 147 | */ 148 | setDateTime(dateTime: SCTS) { 149 | this._dateTime = dateTime; 150 | return this; 151 | } 152 | 153 | /** 154 | * Retrieves the discharge time of the SMS message. 155 | * 156 | * The discharge time indicates when the message delivery process was completed, whether successfully 157 | * or not, providing precise timing information about the delivery status. 158 | * 159 | * @returns The SCTS (Service Center Time Stamp) instance representing the discharge time 160 | */ 161 | get discharge() { 162 | return this._discharge; 163 | } 164 | 165 | /** 166 | * Sets the discharge time of the SMS message. 167 | * 168 | * Updates the discharge time to accurately reflect when the message delivery process concluded, 169 | * essential for detailed delivery reporting. 170 | * 171 | * @param discharge The new SCTS instance representing the discharge time 172 | * @returns The instance of this Report, allowing for method chaining 173 | */ 174 | setDischarge(discharge: SCTS) { 175 | this._discharge = discharge; 176 | return this; 177 | } 178 | 179 | /** 180 | * Retrieves the status of the SMS message delivery. 181 | * 182 | * This status provides detailed information about the outcome of the delivery attempt, 183 | * such as success, failure, or specific error conditions. 184 | * 185 | * @returns The status code representing the delivery outcome 186 | */ 187 | get status() { 188 | return this._status; 189 | } 190 | 191 | /** 192 | * Sets the status of the SMS message delivery. 193 | * 194 | * Updates the delivery status to accurately represent the outcome of the delivery attempt, 195 | * critical for applications requiring confirmation of message delivery. 196 | * 197 | * @param status The new status code indicating the delivery outcome 198 | * @returns The instance of this Report, allowing for method chaining 199 | */ 200 | setStatus(status: number) { 201 | this._status = status; 202 | return this; 203 | } 204 | } 205 | 206 | export type ReportOptions = PDUOptions & { 207 | type?: ReportType; 208 | }; 209 | -------------------------------------------------------------------------------- /tests/parsing.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test } from 'vitest'; 2 | import { parse } from '../src/index'; 3 | import { 4 | expectAddress, 5 | expectDataCodingScheme, 6 | expectDeliver, 7 | expectDeliverOrSubmit, 8 | expectServiceCenterAddress, 9 | expectServiceCenterTimeStamp, 10 | expectSubmit, 11 | expectUserData, 12 | expectUserDataHeader 13 | } from './utils/checkPdu'; 14 | 15 | describe('SMS PDU parser', () => { 16 | test('should parse a simple Deliver PDU correctly', () => { 17 | const pduStr = '07919730071111F1000B919746121611F10000811170021222230DC8329BFD6681EE6F399B1C02'; 18 | const parsedPdu = parse(pduStr); 19 | 20 | expectDeliver(parsedPdu); 21 | expectServiceCenterAddress(parsedPdu, '79037011111'); 22 | expectAddress(parsedPdu, '79642161111'); 23 | expectServiceCenterTimeStamp(parsedPdu, '2018-11-07T20:21:22+08:00'); 24 | expectUserData(parsedPdu, { text: 'Hello, world!' }); 25 | }); 26 | 27 | test('should parse Deliver PDU with negative time-zone offset', () => { 28 | const pduStr = '07919730071111F1000B919746121611F100008111700212222B0DC8329BFD6681EE6F399B1C02'; 29 | const parsedPdu = parse(pduStr); 30 | 31 | expectDeliver(parsedPdu); 32 | expectServiceCenterTimeStamp(parsedPdu, '2018-11-07T20:21:22-08:00'); 33 | }); 34 | 35 | test.each([ 36 | { 37 | name: '+', 38 | pduStr: '001100039199F90000FF1A4937BD2C7787E9E9B73BCC06C1D16F7719E4AEB7C56539', 39 | address: '+999', 40 | text: 'International phone number' 41 | }, 42 | { 43 | name: '00', 44 | pduStr: '001100039199F90000FF1A4937BD2C7787E9E9B73BCC06C1D16F7719E4AEB7C56539', 45 | address: '00999', 46 | text: 'International phone number' 47 | } 48 | ])('should parse Deliver/Submit PDU with international "$name" prefix', ({ pduStr, address, text }) => { 49 | const parsedPdu = parse(pduStr); 50 | 51 | expectDeliverOrSubmit(parsedPdu); 52 | expectAddress(parsedPdu, address); 53 | expectUserData(parsedPdu, { text }); 54 | }); 55 | 56 | test('should parse a non-international Submit PDU correctly', () => { 57 | const pduStr = '000100038199F9000005C8329BFD06'; 58 | const parsedPdu = parse(pduStr); 59 | 60 | expectSubmit(parsedPdu); 61 | expectAddress(parsedPdu, '999'); 62 | expectUserData(parsedPdu, { text: 'Hello' }); 63 | }); 64 | 65 | test('should parse Deliver PDU with alphanumeric originator address', () => { 66 | const pduStr = '07911326060032F0000DD0D432DBFC96D30100001121313121114012D7327BFC6E9741F437885A669BDF723A'; 67 | const parsedPdu = parse(pduStr); 68 | 69 | expectDeliver(parsedPdu); 70 | expectAddress(parsedPdu, 'Telfort'); 71 | expectServiceCenterAddress(parsedPdu, '31626000230'); 72 | expectServiceCenterTimeStamp(parsedPdu, '2011-12-13T13:12:11+01:00'); 73 | expectUserData(parsedPdu, { text: 'Welcome to Telfort' }); 74 | }); 75 | 76 | test('should parse Flash SMS Deliver PDU correctly', () => { 77 | const pduStr = '07919730071111F1000B919746121611F10010811170021222231054747A0E4ACF416190991D9EA343'; 78 | const parsedPdu = parse(pduStr); 79 | 80 | expectDeliver(parsedPdu); 81 | expectAddress(parsedPdu, '79642161111'); 82 | expectDataCodingScheme(parsedPdu, 0x10); 83 | expectUserData(parsedPdu, { text: 'This is a flash!' }); 84 | }); 85 | 86 | test.each([ 87 | { 88 | pduStr: '07919730071111F1000B919746121611F10000811170021222230A1B5E583C2697CD1B1F', 89 | text: '[abcdef]', 90 | size: 10 91 | }, 92 | { 93 | pduStr: '07919730071111F1000B919746121611F1000081117002122223081B14BD3CA76F52', 94 | text: '{test}', 95 | size: 8 96 | } 97 | ])('should parse extended 7-bit symbol "$text"', ({ pduStr, text, size }) => { 98 | const parsedPdu = parse(pduStr); 99 | 100 | expectDeliverOrSubmit(parsedPdu); 101 | expectUserData(parsedPdu, { text, size }); 102 | }); 103 | 104 | test('should parse UCS2 encoded Deliver PDU correctly', () => { 105 | const pduStr = '07919730071111F1000B919746121611F100088111800212222318041F04400438043204350442002C0020043C043804400021'; 106 | const parsedPdu = parse(pduStr); 107 | 108 | expectDeliver(parsedPdu); 109 | 110 | // Russian: "Hello, world!" 111 | expectUserData(parsedPdu, { text: '\u041f\u0440\u0438\u0432\u0435\u0442, \u043c\u0438\u0440!', size: 24 }); 112 | }); 113 | 114 | test('should parse lowercase-hex PDU string correctly', () => { 115 | const pduStr = '07919730071111f1400b919746121611f100008111701222822310050a03000410846f3619f476b3f3'; 116 | const parsedPdu = parse(pduStr); 117 | 118 | expectDeliver(parsedPdu); 119 | expectUserData(parsedPdu, { text: 'Bold only' }); 120 | }); 121 | 122 | test.each([ 123 | { 124 | name: 'bold, italic, underline, strikethrough', 125 | pduStr: '07919730071111F1400B919746121611F100008111701222322342140A030004100A030606200A030E09400A031C0D80C2379BCC0225E961767ACC0255DDE4B29C9D76974161371934A5CBD3EB321D2D7FD7CF6817', 126 | text: 'Bold, Italic, Underline and Strikethrough.' 127 | }, 128 | { 129 | name: 'bold only', 130 | pduStr: '07919730071111F1400B919746121611F100008111701222822310050A03000410846F3619F476B3F3', 131 | text: 'Bold only' 132 | } 133 | ])('should parse EMS formatted text ($name)', ({ pduStr, text }) => { 134 | const parsedPdu = parse(pduStr); 135 | 136 | expectDeliverOrSubmit(parsedPdu); 137 | expectUserData(parsedPdu, { text }); 138 | }); 139 | 140 | test.each([ 141 | { 142 | name: 'segment 1 of 2', 143 | pduStr: '07919730071111F1400B919746121611F10000100161916223230D0500032E020190E175DD1D06', 144 | pointer: 0x2e, 145 | segments: 2, 146 | current: 1, 147 | text: 'Hakuna' 148 | }, 149 | { 150 | name: 'segment 2 of 2', 151 | pduStr: '07919730071111F1400B919746121611F10000100161916233230E0500032E020240ED303D4C0F03', 152 | pointer: 0x2e, 153 | segments: 2, 154 | current: 2, 155 | text: ' matata' 156 | } 157 | ])('should parse concatenated 8-bit ref, $name', ({ pduStr, pointer, segments, current, text }) => { 158 | const parsedPdu = parse(pduStr); 159 | 160 | expectDeliverOrSubmit(parsedPdu); 161 | expectUserDataHeader(parsedPdu, { pointer, segments, current }); 162 | expectUserData(parsedPdu, { text }); 163 | }); 164 | 165 | test.each([ 166 | { 167 | name: '(0x1234), segment 1 of 2', 168 | pduStr: '07919730071111F1400B919746121611F10000811170021222230E06080412340201C8329BFD6601', 169 | pointer: 0x1234, 170 | segments: 2, 171 | current: 1, 172 | text: 'Hello,' 173 | }, 174 | { 175 | name: '(0x1234), segment 2 of 2', 176 | pduStr: '07919730071111F1400B919746121611F10000811170021232230F06080412340202A0FB5BCE268700', 177 | pointer: 0x1234, 178 | segments: 2, 179 | current: 2, 180 | text: ' world!' 181 | }, 182 | { 183 | name: '(0x1235), segment 1 of 2', 184 | pduStr: '07919730071111F1400B919746121611F10000811170021222230B06080412350201C8340B', 185 | pointer: 0x1235, 186 | segments: 2, 187 | current: 1, 188 | text: 'Hi,' 189 | }, 190 | { 191 | name: '(0x1235), segment 2 of 2', 192 | pduStr: '07919730071111F1400B919746121611F10000811170021232230F06080412350202A0FB5BCE268700', 193 | pointer: 0x1235, 194 | segments: 2, 195 | current: 2, 196 | text: ' world!' 197 | }, 198 | { 199 | name: '(0x1234), segment 1 of 3', 200 | pduStr: '07919730071111F1400B919746121611F10000811170021222230E060804123403015774987E9A03', 201 | pointer: 0x1234, 202 | segments: 3, 203 | current: 1, 204 | text: "What's" 205 | }, 206 | { 207 | name: '(0x1234), segment 2 of 3', 208 | pduStr: '07919730071111F1400B919746121611F10000811170021232230C06080412340302A03A9C05', 209 | pointer: 0x1234, 210 | segments: 3, 211 | current: 2, 212 | text: ' up,' 213 | }, 214 | { 215 | name: '(0x1234), segment 3 of 3', 216 | pduStr: '07919730071111F1400B919746121611F10000811170021242230D06080412340303A076D8FD03', 217 | pointer: 0x1234, 218 | segments: 3, 219 | current: 3, 220 | text: ' man?' 221 | } 222 | ])('should parse concatenated 16-bit $name', ({ pduStr, pointer, segments, current, text }) => { 223 | const parsedPdu = parse(pduStr); 224 | 225 | expectDeliverOrSubmit(parsedPdu); 226 | expectUserDataHeader(parsedPdu, { pointer, segments, current }); 227 | expectUserData(parsedPdu, { text }); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /src/utils/DCS.ts: -------------------------------------------------------------------------------- 1 | import { Helper } from './Helper'; 2 | 3 | /** 4 | * Represents the Data Coding Scheme (DCS) of an SMS message. 5 | * 6 | * Defines how the message content is encoded, affecting character set selection and special message features. 7 | * It plays a key role in ensuring that the message content is correctly displayed on the receiving device. 8 | */ 9 | export class DCS { 10 | /* 11 | * GSM 03.38 V7.0.0 (1998-07). 12 | */ 13 | 14 | static readonly CLASS_NONE = 0x00; 15 | static readonly CLASS_MOBILE_EQUIPMENT = 0x01; 16 | static readonly CLASS_SIM_SPECIFIC_MESSAGE = 0x02; 17 | static readonly CLASS_TERMINAL_EQUIPMENT = 0x03; 18 | 19 | static readonly INDICATION_TYPE_VOICEMAIL = 0x00; 20 | static readonly INDICATION_TYPE_FAX = 0x01; 21 | static readonly INDICATION_TYPE_EMAIL = 0x02; 22 | static readonly INDICATION_TYPE_OTHER = 0x03; 23 | 24 | static readonly ALPHABET_DEFAULT = 0x00; 25 | static readonly ALPHABET_8BIT = 0x01; 26 | static readonly ALPHABET_UCS2 = 0x02; // 16 bit unicode 27 | static readonly ALPHABET_RESERVED = 0x03; 28 | 29 | private _encodeGroup: number; 30 | private _dataEncoding: number; 31 | private _compressedText: boolean; 32 | private _textAlphabet: number; 33 | private _useMessageClass: boolean; 34 | private _classMessage: number; 35 | private _discardMessage: boolean; 36 | private _storeMessage: boolean; 37 | private _storeMessageUCS2: boolean; 38 | private _dataCodingAndMessageClass: boolean; 39 | private _messageIndication: number; 40 | private _messageIndicationType: number; 41 | 42 | constructor(options: DCSOptions = {}) { 43 | this._encodeGroup = options.encodeGroup || 0x00; 44 | this._dataEncoding = options.dataEncoding || 0x00; 45 | this._compressedText = options.compressedText || false; 46 | this._textAlphabet = options.textAlphabet || DCS.ALPHABET_DEFAULT; 47 | this._useMessageClass = options.useMessageClass || false; 48 | this._classMessage = options.classMessage || DCS.CLASS_NONE; 49 | this._discardMessage = options.discardMessage || false; 50 | this._storeMessage = options.storeMessage || false; 51 | this._storeMessageUCS2 = options.storeMessageUCS2 || false; 52 | this._dataCodingAndMessageClass = options.dataCodingAndMessageClass || false; 53 | this._messageIndication = options.messageIndication || 0; 54 | this._messageIndicationType = options.messageIndicationType || 0; 55 | } 56 | 57 | /* 58 | * ================================================ 59 | * Getter & Setter 60 | * ================================================ 61 | */ 62 | 63 | get encodeGroup() { 64 | return this._encodeGroup; 65 | } 66 | 67 | get dataEncoding() { 68 | return this._dataEncoding; 69 | } 70 | 71 | get compressedText() { 72 | return this._compressedText; 73 | } 74 | 75 | get dataCodingAndMessageClass() { 76 | return this._dataCodingAndMessageClass; 77 | } 78 | 79 | get discardMessage() { 80 | return this._discardMessage; 81 | } 82 | 83 | setDiscardMessage() { 84 | this._discardMessage = true; 85 | return this; 86 | } 87 | 88 | get storeMessage() { 89 | return this._storeMessage; 90 | } 91 | 92 | setStoreMessage() { 93 | this._storeMessage = true; 94 | return this; 95 | } 96 | 97 | get storeMessageUCS2() { 98 | return this._storeMessageUCS2; 99 | } 100 | 101 | setStoreMessageUCS2() { 102 | this._storeMessageUCS2 = true; 103 | return this; 104 | } 105 | 106 | get messageIndication() { 107 | return this._messageIndication; 108 | } 109 | 110 | setMessageIndication(indication: number) { 111 | this._messageIndication = 1 & indication; 112 | return this; 113 | } 114 | 115 | get messageIndicationType() { 116 | return this._messageIndicationType; 117 | } 118 | 119 | setMessageIndicationType(type: number) { 120 | this._messageIndicationType = 0x03 & type; 121 | 122 | switch (this._messageIndicationType) { 123 | case DCS.INDICATION_TYPE_VOICEMAIL: 124 | break; 125 | 126 | case DCS.INDICATION_TYPE_FAX: 127 | break; 128 | 129 | case DCS.INDICATION_TYPE_EMAIL: 130 | break; 131 | 132 | case DCS.INDICATION_TYPE_OTHER: 133 | break; 134 | 135 | default: 136 | throw new Error('node-pdu: Wrong indication type!'); 137 | } 138 | 139 | return this; 140 | } 141 | 142 | get textTextCompressed() { 143 | return this._compressedText; 144 | } 145 | 146 | setTextCompressed(compressed = true) { 147 | this._compressedText = compressed; 148 | return this; 149 | } 150 | 151 | get textAlphabet() { 152 | return this._textAlphabet; 153 | } 154 | 155 | setTextAlphabet(alphabet: number) { 156 | this._textAlphabet = 0x03 & alphabet; 157 | 158 | switch (this._textAlphabet) { 159 | case DCS.ALPHABET_DEFAULT: 160 | break; 161 | 162 | case DCS.ALPHABET_8BIT: 163 | break; 164 | 165 | case DCS.ALPHABET_UCS2: 166 | break; 167 | 168 | case DCS.ALPHABET_RESERVED: 169 | break; 170 | 171 | default: 172 | throw new Error('node-pdu: Wrong alphabet!'); 173 | } 174 | 175 | return this; 176 | } 177 | 178 | get classMessage() { 179 | return this._classMessage; 180 | } 181 | 182 | setClass(cls: number) { 183 | this.setUseMessageClass(); 184 | this._classMessage = 0x03 & cls; 185 | 186 | switch (this._classMessage) { 187 | case DCS.CLASS_NONE: 188 | this.setUseMessageClass(false); 189 | break; 190 | 191 | case DCS.CLASS_MOBILE_EQUIPMENT: 192 | break; 193 | 194 | case DCS.CLASS_SIM_SPECIFIC_MESSAGE: 195 | break; 196 | 197 | case DCS.CLASS_TERMINAL_EQUIPMENT: 198 | break; 199 | 200 | default: 201 | throw new Error('node-pdu: Wrong class type!'); 202 | } 203 | 204 | return this; 205 | } 206 | 207 | get useMessageClass() { 208 | return this._useMessageClass; 209 | } 210 | 211 | setUseMessageClass(use = true) { 212 | this._useMessageClass = use; 213 | return this; 214 | } 215 | 216 | /* 217 | * ================================================ 218 | * Public functions 219 | * ================================================ 220 | */ 221 | 222 | getValue() { 223 | this._encodeGroup = 0x00; 224 | 225 | // set data encoding, from alphabet and message class 226 | this._dataEncoding = (this._textAlphabet << 2) | this._classMessage; 227 | 228 | // set message class bit 229 | if (this._useMessageClass) { 230 | this._encodeGroup |= 1 << 0; 231 | } 232 | 233 | // set is compressed bit 234 | if (this._compressedText) { 235 | this._encodeGroup |= 1 << 1; 236 | } 237 | 238 | // change encoding format 239 | if (this._discardMessage || this._storeMessage || this._storeMessageUCS2) { 240 | this._dataEncoding = 0x00; 241 | 242 | // set indication 243 | if (this._messageIndication) { 244 | this._dataEncoding |= 1 << 3; 245 | 246 | // set message indication type 247 | this._dataEncoding |= this._messageIndicationType; 248 | } 249 | } 250 | 251 | // Discard Message 252 | if (this._discardMessage) { 253 | this._encodeGroup = 0x0c; 254 | } 255 | 256 | // Store Message 257 | if (this._storeMessage) { 258 | this._encodeGroup = 0x0d; 259 | } 260 | 261 | // Store Message UCS2 262 | if (this._storeMessageUCS2) { 263 | this._encodeGroup = 0x0e; 264 | } 265 | 266 | // Data Coding and Message Class 267 | if (this._dataCodingAndMessageClass) { 268 | // set bits to 1 269 | this._encodeGroup = 0x0f; 270 | 271 | // only class message 272 | this._dataEncoding = 0x03 & this._classMessage; 273 | 274 | // check encoding 275 | switch (this._textAlphabet) { 276 | case DCS.ALPHABET_8BIT: 277 | this._dataEncoding |= 1 << 2; 278 | break; 279 | case DCS.ALPHABET_DEFAULT: 280 | // bit is set to 0 281 | break; 282 | default: 283 | break; 284 | } 285 | } 286 | 287 | // return byte value 288 | return ((0x0f & this._encodeGroup) << 4) | (0x0f & this._dataEncoding); 289 | } 290 | 291 | toString() { 292 | return Helper.toStringHex(this.getValue()); 293 | } 294 | } 295 | 296 | export type DCSOptions = { 297 | encodeGroup?: number; 298 | dataEncoding?: number; 299 | compressedText?: boolean; 300 | textAlphabet?: number; 301 | useMessageClass?: boolean; 302 | classMessage?: number; 303 | discardMessage?: boolean; 304 | storeMessage?: boolean; 305 | storeMessageUCS2?: boolean; 306 | dataCodingAndMessageClass?: boolean; 307 | messageIndication?: number; 308 | messageIndicationType?: number; 309 | }; 310 | -------------------------------------------------------------------------------- /src/Submit.ts: -------------------------------------------------------------------------------- 1 | import { Data } from './utils/Data/Data'; 2 | import { Helper } from './utils/Helper'; 3 | import { PDU, type PDUOptions } from './utils/PDU'; 4 | import type { SCA } from './utils/SCA/SCA'; 5 | import { SubmitType } from './utils/Type/SubmitType'; 6 | import { VP } from './utils/VP'; 7 | 8 | /** 9 | * Represents a GSM SMS Submit PDU. 10 | * 11 | * Facilitates sending SMS messages from devices to the SMSC. It's critical for any application or service 12 | * that needs to initiate outbound text messages. 13 | */ 14 | export class Submit extends PDU { 15 | private _type: SubmitType; 16 | private _data: Data; 17 | private _messageReference: number; 18 | private _validityPeriod: VP; 19 | 20 | /** 21 | * Constructs a SMS Submit PDU instance. 22 | * 23 | * @param address The recipents address as a string or an instance of SCA 24 | * @param data The message content as a string or an instance of Data 25 | * @param options An object containing optional parameters for the Submit instance 26 | */ 27 | constructor(address: string | SCA, data: string | Data, options: SubmitOptions = {}) { 28 | super(address, options); 29 | 30 | this._type = options.type || new SubmitType(); 31 | this._data = this.findData(data); 32 | this._messageReference = options.messageReference || 0x00; 33 | this._validityPeriod = options.validityPeriod || new VP(); 34 | } 35 | 36 | /* 37 | * ================================================ 38 | * Getter & Setter 39 | * ================================================ 40 | */ 41 | 42 | /** 43 | * Retrieves the message type for this Submit PDU. 44 | * 45 | * This property represents the specific characteristics and parameters related to the submission of the SMS message, 46 | * such as whether it's a standard message, a status report request, etc. 47 | * 48 | * @returns The current SubmitType instance, defining the message type 49 | */ 50 | get type() { 51 | return this._type; 52 | } 53 | 54 | /** 55 | * Sets the message type for this Submit PDU. 56 | * 57 | * Allows for specifying the type of SMS submit message, adjusting its parameters like whether it requests 58 | * a status report, among other options. 59 | * 60 | * @param type The new SubmitType instance to set as the message type 61 | * @returns The instance of this Submit, allowing for method chaining 62 | */ 63 | setType(type: SubmitType) { 64 | this._type = type; 65 | return this; 66 | } 67 | 68 | /** 69 | * Retrieves the data/content of the SMS message. 70 | * 71 | * This property holds the actual message content that will be sent, which can be in various formats 72 | * (e.g., plain text, binary data) depending on the data coding scheme. 73 | * 74 | * @returns The current Data instance containing the message content 75 | */ 76 | get data() { 77 | return this._data; 78 | } 79 | 80 | /** 81 | * Sets the data/content of the SMS message. 82 | * 83 | * This method allows the message content to be updated, accepting either a string (which will be 84 | * converted to a Data instance internally) or a Data instance directly. 85 | * 86 | * @param data The new message content, either as a string or a Data instance 87 | * @returns The instance of this Submit, allowing for method chaining 88 | */ 89 | setData(data: string | Data) { 90 | this._data = this.findData(data); 91 | return this; 92 | } 93 | 94 | /** 95 | * Retrieves the message reference number for this Submit PDU. 96 | * 97 | * The message reference is a unique identifier for the message, used for tracking purposes and 98 | * to correlate delivery reports with the original messages. 99 | * 100 | * @returns The current message reference number 101 | */ 102 | get messageReference() { 103 | return this._messageReference; 104 | } 105 | 106 | /** 107 | * Sets the message reference number for this Submit PDU. 108 | * 109 | * This method allows setting a custom message reference number for tracking and correlation purposes. 110 | * 111 | * @param messageReference The new message reference number 112 | * @returns The instance of this Submit, allowing for method chaining 113 | */ 114 | setMessageReference(messageReference: number) { 115 | this._messageReference = messageReference; 116 | return this; 117 | } 118 | 119 | /** 120 | * Retrieves the Validity Period (VP) for the SMS message. 121 | * 122 | * The validity period indicates how long the message is valid for delivery before it expires. 123 | * This can be specified as an absolute date/time or as a time interval from the submission. 124 | * 125 | * @returns The current validity period as a VP instance 126 | */ 127 | get validityPeriod() { 128 | return this._validityPeriod; 129 | } 130 | 131 | /** 132 | * Sets the Validity Period (VP) for the SMS message. 133 | * 134 | * This method allows specifying how long the message is valid for delivery before it expires. 135 | * The validity period can be set as an absolute date/time or as a time interval from the submission. 136 | * 137 | * @param value The new validity period, either as a VP instance, a string (for absolute date/time), or a number (for time interval) 138 | * @returns The instance of this Submit, allowing for method chaining 139 | */ 140 | setValidityPeriod(value: VP | string | number) { 141 | if (value instanceof VP) { 142 | this._validityPeriod = value; 143 | return this; 144 | } 145 | 146 | this._validityPeriod = new VP(); 147 | 148 | if (typeof value === 'string') { 149 | this._validityPeriod.setDateTime(value); 150 | } else { 151 | this._validityPeriod.setInterval(value); 152 | } 153 | 154 | return this; 155 | } 156 | 157 | /* 158 | * ================================================ 159 | * Private functions 160 | * ================================================ 161 | */ 162 | 163 | private findData(data: string | Data) { 164 | if (data instanceof Data) { 165 | return data; 166 | } 167 | 168 | return new Data().setData(data, this); 169 | } 170 | 171 | /* 172 | * ================================================ 173 | * Public functions 174 | * ================================================ 175 | */ 176 | 177 | /** 178 | * Retrieves all parts of the message data. 179 | * 180 | * For messages that are split into multiple parts (e.g., concatenated SMS), this method returns 181 | * all parts as an array. 182 | * 183 | * @returns An array of Data parts representing the message content 184 | */ 185 | getParts() { 186 | return this._data.parts; 187 | } 188 | 189 | /** 190 | * Retrieves all parts of the message as strings. 191 | * 192 | * This method is useful for concatenated messages, converting each part of the message data 193 | * into a string format based on the current data coding scheme. 194 | * 195 | * @returns An array of strings, each representing a part of the message content 196 | */ 197 | getPartStrings() { 198 | return this._data.parts.map((part) => part.toString(this)); 199 | } 200 | 201 | /** 202 | * Generates a string representation of the start of the PDU. 203 | * 204 | * This method constructs the initial part of the PDU string, including information like the 205 | * service center address, message type, message reference number, sender's address, protocol identifier, 206 | * data coding scheme, and validity period. 207 | * 208 | * @returns A string representing the start of the PDU 209 | */ 210 | getStart() { 211 | let str = ''; 212 | 213 | str += this.serviceCenterAddress.toString(); 214 | str += this._type.toString(); 215 | str += Helper.toStringHex(this._messageReference); 216 | str += this.address.toString(); 217 | str += Helper.toStringHex(this.protocolIdentifier.getValue()); 218 | str += this.dataCodingScheme.toString(); 219 | str += this._validityPeriod.toString(this); 220 | 221 | return str; 222 | } 223 | 224 | /** 225 | * Converts the entire Submit PDU into a string representation. 226 | * 227 | * This method is intended to provide a complete textual representation of the Submit PDU, 228 | * including all headers and the message content, formatted according to the PDU protocol. 229 | * 230 | * @returns A string representation of the Submit PDU 231 | */ 232 | toString() { 233 | return this.getParts() 234 | .map((part) => part.toString(this)) 235 | .join('\n'); 236 | } 237 | } 238 | 239 | export type SubmitOptions = PDUOptions & { 240 | type?: SubmitType; 241 | messageReference?: number; 242 | validityPeriod?: VP; 243 | }; 244 | -------------------------------------------------------------------------------- /src/utils/Data/Data.ts: -------------------------------------------------------------------------------- 1 | import type { Deliver } from '../../Deliver'; 2 | import type { Submit } from '../../Submit'; 3 | import { DCS } from '../DCS'; 4 | import { Helper } from '../Helper'; 5 | import { Header } from './Header'; 6 | import { Part } from './Part'; 7 | 8 | /** 9 | * Represents the data content of an SMS message. 10 | * 11 | * Holds the actual message content, whether text or binary data. It is central to the purpose of SMS, 12 | * conveying the intended information from sender to recipient. 13 | */ 14 | export class Data { 15 | static readonly HEADER_SIZE = 7; // UDHL + UDH 16 | 17 | private _data: string; 18 | private _size: number; 19 | private _parts: Part[]; 20 | private _isUnicode: boolean; 21 | 22 | /** 23 | * Constructs a Data instance. 24 | * @param options An object containing optional parameters for the Data instance 25 | */ 26 | constructor(options: DataOptions = {}) { 27 | this._size = options.size || 0; 28 | this._data = options.data || ''; 29 | this._parts = options.parts || []; 30 | this._isUnicode = options.isUnicode || false; 31 | } 32 | 33 | /* 34 | * ================================================ 35 | * Getter & Setter 36 | * ================================================ 37 | */ 38 | 39 | /** 40 | * Gets the raw data of the SMS message. 41 | * 42 | * This property holds the actual content of the message, which could be text or binary data, 43 | * depending on how the message was encoded. 44 | * 45 | * @returns The raw data as a string 46 | */ 47 | get data() { 48 | return this._data; 49 | } 50 | 51 | /** 52 | * Retrieves the size of the message data. 53 | * 54 | * The size is determined based on the encoding and content of the message. It reflects the 55 | * number of characters or bytes. 56 | * 57 | * @returns The size of the data 58 | */ 59 | get size() { 60 | return this._size; 61 | } 62 | 63 | /** 64 | * Provides access to the individual parts of the message. 65 | * 66 | * For longer messages that are split into multiple parts (segments), this getter allows access 67 | * to each part. Each part contains a portion of the message data along with metadata. 68 | * 69 | * @returns An array of Part instances, each representing a segment of the message 70 | */ 71 | get parts() { 72 | return this._parts; 73 | } 74 | 75 | /** 76 | * Indicates whether the message data is encoded using Unicode. 77 | * 78 | * This property is true if the message content includes characters that require Unicode encoding 79 | * (e.g., non-Latin characters). Otherwise, it's false. 80 | * 81 | * @returns A boolean indicating if the message data is Unicode. 82 | */ 83 | get isUnicode() { 84 | return this._isUnicode; 85 | } 86 | 87 | /** 88 | * Sets the data for the SMS message. 89 | * 90 | * This method encodes the provided data string according to the specified PDU type (Deliver or Submit) 91 | * and updates the message parts accordingly. It handles encoding, part splitting, and header preparation. 92 | * 93 | * @param data The new message data as a string 94 | * @param pdu The PDU instance (Deliver or Submit) associated with this data 95 | * @returns The instance of this Data, allowing for method chaining 96 | */ 97 | setData(data: string, pdu: Deliver | Submit) { 98 | this._data = data; 99 | 100 | // encode message 101 | this.checkData(); 102 | 103 | // preapre parts 104 | this.prepareParts(pdu); 105 | 106 | return this; 107 | } 108 | 109 | /* 110 | * ================================================ 111 | * Private functions 112 | * ================================================ 113 | */ 114 | 115 | private checkData() { 116 | // set is unicode to false 117 | this._isUnicode = false; 118 | // set zero size 119 | this._size = 0; 120 | 121 | // check message 122 | for (let i = 0; i < this._data.length; i++) { 123 | // get byte 124 | const byte = Helper.order(this._data.substring(i, i + 1)); 125 | 126 | if (byte > 0xc0) { 127 | this._isUnicode = true; 128 | } 129 | 130 | this._size++; 131 | } 132 | } 133 | 134 | private prepareParts(pdu: Deliver | Submit) { 135 | let headerSize = Data.HEADER_SIZE; 136 | let max = Helper.limitNormal; 137 | 138 | if (this._isUnicode) { 139 | // max length sms to unicode 140 | max = Helper.limitUnicode; 141 | 142 | // can't compress message 143 | pdu.dataCodingScheme 144 | .setTextCompressed(false) // no compress 145 | .setTextAlphabet(DCS.ALPHABET_UCS2); // type alphabet is UCS2 146 | } 147 | 148 | // if message is compressed 149 | if (pdu.dataCodingScheme.compressedText) { 150 | max = Helper.limitCompress; 151 | headerSize++; 152 | } 153 | 154 | const parts = this.splitMessage(max - headerSize); 155 | const haveHeader = parts.length > 1; 156 | const uniqID = (Math.random() * 0x10000) | 0; 157 | 158 | // message will be splited, need headers 159 | if (haveHeader) { 160 | pdu.type.setUserDataHeader(1); 161 | } 162 | 163 | parts.forEach((text, index) => { 164 | const header = haveHeader ? new Header({ SEGMENTS: parts.length, CURRENT: index + 1, POINTER: uniqID }) : undefined; 165 | 166 | const tmp = (() => { 167 | switch (pdu.dataCodingScheme.textAlphabet) { 168 | case DCS.ALPHABET_DEFAULT: 169 | return Helper.encode7Bit(text); 170 | 171 | case DCS.ALPHABET_8BIT: 172 | return Helper.encode8Bit(text); 173 | 174 | case DCS.ALPHABET_UCS2: 175 | return Helper.encode16Bit(text); 176 | 177 | default: 178 | throw new Error('node-pdu: Unknown alphabet!'); 179 | } 180 | })(); 181 | 182 | let size = tmp.length; 183 | const data = tmp.result; 184 | 185 | if (haveHeader) { 186 | if (pdu.dataCodingScheme.textAlphabet === DCS.ALPHABET_DEFAULT) { 187 | // When using 7bit encoding (ALPHABET_DEFAULT), the UDH must be padded into septets. 188 | // 1 byte = 8 bits, so we calculate the UDH size in septets: ceil(bytes * 8 / 7) 189 | // This ensures the user data length is correct and no character is lost. 190 | size += Math.ceil((headerSize * 8) / 7); 191 | } else { 192 | // For 8bit and UCS2, header size is already in bytes and can be added directly. 193 | size += headerSize; 194 | } 195 | } 196 | 197 | this._parts.push(new Part(data, size, text, header)); 198 | }); 199 | } 200 | 201 | private partExists(part: Part) { 202 | for (const p of this._parts) { 203 | if (part.header === null || p.header === null) { 204 | throw new Error('node-pdu: Part is missing a header!'); 205 | } 206 | 207 | if (part.header.getPointer() !== p.header.getPointer() || part.header.getSegments() !== p.header.getSegments()) { 208 | throw new Error('node-pdu: Part from different message!'); 209 | } 210 | 211 | if (p.header.getCurrent() === part.header.getCurrent()) { 212 | return true; 213 | } 214 | } 215 | 216 | return false; 217 | } 218 | 219 | private sortParts() { 220 | this._data = this._parts 221 | .sort((p1, p2) => (p1.header?.getCurrent() || 1) - (p2.header?.getCurrent() || 1)) 222 | .map((part) => part.text) 223 | .join(''); 224 | } 225 | 226 | private splitMessage(size: number) { 227 | const data = []; 228 | 229 | for (let i = 0; i < this._data.length; i += size) { 230 | data.push(this._data.substring(i, i + size)); 231 | } 232 | 233 | return data; 234 | } 235 | 236 | /* 237 | * ================================================ 238 | * Public functions 239 | * ================================================ 240 | */ 241 | 242 | /** 243 | * Retrieves the textual content of the SMS message. 244 | * 245 | * This method decodes the raw data into a readable text format, considering the encoding scheme 246 | * (e.g., GSM 7-bit, UCS-2) used for the message. 247 | * 248 | * @returns The decoded text of the message 249 | */ 250 | getText() { 251 | return this.data; 252 | } 253 | 254 | /** 255 | * Appends the parts from another PDU to this data instance. 256 | * 257 | * This method is used to combine the parts of another message (Deliver or Submit) into this one, 258 | * effectively concatenating the messages. It ensures parts are added only once and sorts them. 259 | * 260 | * @param pdu The PDU instance (Deliver or Submit) whose parts are to be appended 261 | */ 262 | append(pdu: Deliver | Submit) { 263 | pdu.getParts().forEach((part) => { 264 | if (!this.partExists(part)) { 265 | this._parts.push(part); 266 | } 267 | }); 268 | 269 | this.sortParts(); 270 | } 271 | } 272 | 273 | export type DataOptions = { 274 | data?: string; 275 | size?: number; 276 | parts?: Part[]; 277 | isUnicode?: boolean; 278 | }; 279 | -------------------------------------------------------------------------------- /src/utils/Helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A utility class providing static methods for encoding and decoding SMS messages. 3 | * 4 | * This class contains methods for converting text into various encoding formats used in SMS, 5 | * such as GSM 7-bit, 8-bit, and UCS-2 (16-bit). It also includes utility methods 6 | * for handling characters and converting values to hexadecimal strings. 7 | */ 8 | export class Helper { 9 | static readonly ALPHABET_7BIT = 10 | '@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ !"#¤%&\'()*+,-./0123456789:;<=>?¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑÜ`¿abcdefghijklmnopqrstuvwxyzäöñüà'; 11 | static readonly EXTENDED_TABLE = 12 | '````````````````````^```````````````````{}`````\\````````````[~]`|````````````````````````````````````€``````````````````````````'; 13 | 14 | static readonly limitNormal = 140; 15 | static readonly limitCompress = 160; 16 | static readonly limitUnicode = 70; 17 | 18 | private static readonly TEXT_ENCODER = new TextEncoder(); 19 | private static readonly TEXT_DECODER = new TextDecoder(); 20 | 21 | /** 22 | * Converts a hex string to a Uint8Array. 23 | * 24 | * @param hex The hex string to convert 25 | * @returns A Uint8Array representing the hex string 26 | */ 27 | static hexToUint8Array(hex: string): Uint8Array { 28 | if (hex.length % 2 !== 0) { 29 | throw new Error('Hex string must have an even number of characters'); 30 | } 31 | 32 | const bytes = new Uint8Array(hex.length / 2); 33 | 34 | for (let i = 0; i < hex.length; i += 2) { 35 | bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); 36 | } 37 | 38 | return bytes; 39 | } 40 | 41 | /** 42 | * Converts an ASCII string to a Uint8Array. 43 | * 44 | * @param ascii The ASCII string to convert 45 | * @returns A Uint8Array representing the ASCII string 46 | */ 47 | static asciiToUint8Array(ascii: string): Uint8Array { 48 | return this.TEXT_ENCODER.encode(ascii); 49 | } 50 | 51 | /** 52 | * Converts the first two characters of a hexadecimal string to a number. 53 | * 54 | * @param hexStr The hexadecimal string to convert 55 | * @returns The number represented by the first two characters of the hexadecimal string 56 | */ 57 | static getByteFromHex(hexStr: string): number { 58 | return parseInt(hexStr.substring(0, 2), 16); 59 | } 60 | 61 | /** 62 | * Capitalizes the first character of the input string. 63 | * 64 | * @param str The string to capitalize 65 | * @returns The input string with its first character capitalized 66 | */ 67 | static ucfirst(str: string) { 68 | return str.substring(0, 1).toUpperCase() + str.substring(1); 69 | } 70 | 71 | /** 72 | * Returns the Unicode code point of the first character of the input string. 73 | * 74 | * @param char A single character string 75 | * @returns The Unicode code point of the character 76 | */ 77 | static order(char: string) { 78 | return char.charCodeAt(0); 79 | } 80 | 81 | /** 82 | * Returns the character represented by the specified Unicode code point. 83 | * 84 | * @param order The Unicode code point 85 | * @returns A string containing the character represented by the code point 86 | */ 87 | static char(order: number) { 88 | return String.fromCharCode(order); 89 | } 90 | 91 | /** 92 | * Decodes a 16-bit encoded string into a human-readable text. 93 | * 94 | * @param text The 16-bit encoded hexadecimal string 95 | * @returns The decoded text 96 | */ 97 | static decode16Bit(text: string) { 98 | return (text.match(/.{1,4}/g) || []) 99 | .map((hex) => { 100 | const buffer = Helper.hexToUint8Array(hex); 101 | return Helper.char((buffer[0] << 8) | buffer[1]); 102 | }) 103 | .join(''); 104 | } 105 | 106 | /** 107 | * Decodes an 8-bit encoded string into a human-readable text. 108 | * 109 | * @param text The 8-bit encoded hexadecimal string 110 | * @returns The decoded text 111 | */ 112 | static decode8Bit(text: string) { 113 | return (text.match(/.{1,2}/g) || []).map((hex) => Helper.char(Helper.getByteFromHex(hex))).join(''); 114 | } 115 | 116 | /** 117 | * Decodes a 7-bit encoded string into a human-readable text. 118 | * 119 | * @param text The 7-bit encoded hexadecimal string 120 | * @param inLen The length of the input data in septets 121 | * @param alignBits The number of bits for alignment 122 | * 123 | * @returns The decoded text 124 | */ 125 | static decode7Bit(text: string, inLen?: number, alignBits?: number) { 126 | const ret: number[] = []; 127 | const data = Helper.hexToUint8Array(text); 128 | 129 | let dataPos = 0; // Position in the input octets stream 130 | let buf = 0; // Bit buffer, used in FIFO manner 131 | let bufLen = 0; // Amount of buffered bits 132 | let inDone = 0; 133 | let inExt = false; 134 | 135 | // If we have some leading alignment bits then skip them 136 | if (alignBits && data.length) { 137 | alignBits = alignBits % 7; 138 | buf = data[dataPos++]; 139 | buf >>= alignBits; 140 | bufLen = 8 - alignBits; 141 | } 142 | 143 | while (!(bufLen < 7 && dataPos === data.length)) { 144 | if (bufLen < 7) { 145 | if (dataPos === data.length) { 146 | break; 147 | } 148 | 149 | // Move next input octet to the FIFO buffer 150 | buf |= data[dataPos++] << bufLen; 151 | bufLen += 8; 152 | } 153 | 154 | // Fetch next septet from the FIFO buffer 155 | const digit = buf & 0x7f; 156 | 157 | buf >>= 7; 158 | bufLen -= 7; 159 | inDone++; 160 | 161 | if (digit % 128 === 27) { 162 | // Escape character 163 | inExt = true; 164 | } else { 165 | let c = inExt ? Helper.EXTENDED_TABLE.charCodeAt(digit) || 63 : Helper.ALPHABET_7BIT.charCodeAt(digit); 166 | inExt = false; 167 | 168 | if (c < 0x80) { 169 | ret.push(c); 170 | } else if (c < 0x800) { 171 | ret.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); 172 | } else if ( 173 | (c & 0xfc00) === 0xd800 && 174 | digit + 1 < Helper.EXTENDED_TABLE.length && 175 | (Helper.EXTENDED_TABLE.charCodeAt(digit + 1) & 0xfc00) === 0xdc00 176 | ) { 177 | // Surrogate Pair 178 | c = 0x10000 + ((c & 0x03ff) << 10) + (Helper.EXTENDED_TABLE.charCodeAt(digit + 1) & 0x03ff); 179 | ret.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); 180 | } else { 181 | ret.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); 182 | } 183 | } 184 | 185 | // Do we process all input data 186 | if (inLen === undefined) { 187 | // If we have only the final (possibly padding) septet and it's empty 188 | if (dataPos === data.length && bufLen === 7 && !buf) { 189 | break; 190 | } 191 | } else { 192 | if (inDone >= inLen) { 193 | break; 194 | } 195 | } 196 | } 197 | 198 | return this.TEXT_DECODER.decode(new Uint8Array(ret)); 199 | } 200 | 201 | /** 202 | * Encodes a text string into 8-bit hexadecimal PDU format. 203 | * 204 | * @param text The text to encode 205 | * @returns An object containing the length of the encoded text and the result as a hexadecimal string 206 | */ 207 | static encode8Bit(text: string) { 208 | const buffer = Helper.asciiToUint8Array(text); 209 | let result = ''; 210 | 211 | for (let i = 0; i < buffer.length; i++) { 212 | result += Helper.toStringHex(buffer[i]); 213 | } 214 | 215 | return { length: buffer.length, result }; 216 | } 217 | 218 | /** 219 | * Encodes a text string into 7-bit hexadecimal PDU format. 220 | * 221 | * @param text The text to encode 222 | * @param alignBits The number of bits for alignment, if needed 223 | * 224 | * @returns An object containing the length of the encoded text in septets and the result as a hexadecimal string 225 | */ 226 | static encode7Bit(text: string, alignBits = 0) { 227 | let result = ''; 228 | let buf = 0; // Bit buffer, used in FIFO manner 229 | let bufLen = 0; // Amount of buffered bits 230 | let length = 0; // Amount of produced septets 231 | 232 | // Adjust for initial padding if alignBits is specified 233 | bufLen = alignBits; 234 | 235 | for (const symb of text) { 236 | let code: number; 237 | 238 | if ((code = Helper.ALPHABET_7BIT.indexOf(symb)) !== -1) { 239 | // Normal character 240 | buf |= code << bufLen; 241 | bufLen += 7; 242 | length++; 243 | } else if ((code = Helper.EXTENDED_TABLE.indexOf(symb)) !== -1) { 244 | // ESC character (27), then the actual extended character 245 | buf |= 27 << bufLen; 246 | bufLen += 7; 247 | length++; 248 | 249 | // Then add extended character 250 | buf |= code << bufLen; 251 | bufLen += 7; 252 | length++; 253 | } else { 254 | // Replace unknown with space (' '- code 0x20) 255 | buf |= 32 << bufLen; 256 | bufLen += 7; 257 | length++; 258 | } 259 | 260 | while (bufLen >= 8) { 261 | result += Helper.toStringHex(buf & 0xff); 262 | buf >>= 8; 263 | bufLen -= 8; 264 | } 265 | } 266 | 267 | // Write out remaining bits if needed 268 | if (bufLen > 0) { 269 | result += Helper.toStringHex(buf & 0xff); 270 | } 271 | 272 | if (alignBits) { 273 | length++; // Add 1 to length to account for the padding septet 274 | } 275 | 276 | return { length, result }; 277 | } 278 | 279 | /** 280 | * Encodes a text string into 16-bit hexadecimal PDU format. 281 | * 282 | * @param text The text to encode 283 | * @returns An object containing the length of the encoded text in septets and the result as a hexadecimal string 284 | */ 285 | static encode16Bit(text: string) { 286 | let pdu = ''; 287 | 288 | for (let i = 0; i < text.length; i++) { 289 | const byte = Helper.order(text.substring(i, i + 1)); 290 | pdu += Helper.toStringHex(byte, 4); 291 | } 292 | 293 | return { length: text.length * 2, result: pdu }; 294 | } 295 | 296 | /** 297 | * Converts a number to a hexadecimal string with optional zero padding. 298 | * 299 | * @param number The number to convert 300 | * @param fill The minimum length of the resulting string, padded with zeros if necessary 301 | * @returns The number as a hexadecimal string 302 | */ 303 | static toStringHex(number: number, fill = 2) { 304 | return number.toString(16).padStart(fill, '0').toUpperCase(); 305 | } 306 | } 307 | --------------------------------------------------------------------------------