skills/rytass/utils/sms-adapters

sms-adapters

SKILL.md

Taiwan SMS Adapters

This skill provides comprehensive guidance for using @rytass/sms-adapter-* packages to integrate Taiwan SMS service providers.

Overview

All adapters implement the SMSService interface from @rytass/sms, providing a unified API across different providers:

Package Provider Description
@rytass/sms-adapter-every8d Every8D (每日簡訊 / 互動資通) Taiwan SMS gateway by Interactive Communications

Base Interface (@rytass/sms)

All adapters share these core concepts:

SMSService - Main interface for SMS operations:

interface SMSService<
  Request extends SMSRequest,
  SendResponse extends SMSSendResponse,
  MultiTarget extends MultiTargetRequest,
> {
  send(request: Request[]): Promise<SendResponse[]>;
  send(request: Request): Promise<SendResponse>;
  send(request: MultiTarget): Promise<SendResponse[]>;
}

Request Types:

  • SMSRequest - Single SMS message to one recipient
  • MultiTargetRequest - Same message to multiple recipients

Response Status:

  • SUCCESS - Message sent successfully
  • FAILED - Message delivery failed

Taiwan Phone Number Helpers (from @rytass/sms):

// 台灣手機號碼正規表達式
const TAIWAN_PHONE_NUMBER_RE = /^(0|\+?886-?)9\d{2}-?\d{3}-?\d{3}$/;

// 正規化台灣手機號碼(移除非數字字元,將 886 前綴轉為 0)
function normalizedTaiwanMobilePhoneNumber(mobile: string): string;
// '0987-654-321' → '0987654321'
// '+886987654321' → '0987654321'
// '886987654321' → '0987654321'

Installation

# Install base package
npm install @rytass/sms

# Choose the adapter for your provider
npm install @rytass/sms-adapter-every8d

Quick Start

Every8D (每日簡訊 / 互動資通)

import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d';

// Initialize SMS service
const smsService = new SMSServiceEvery8D({
  username: process.env.EVERY8D_USERNAME!,
  password: process.env.EVERY8D_PASSWORD!,
  baseUrl: 'https://api.e8d.tw', // Default API endpoint
  onlyTaiwanMobileNumber: true, // Restrict to Taiwan numbers only
});

// Send single SMS
const result = await smsService.send({
  mobile: '0987654321',
  text: 'Hello! This is a test message.',
});

console.log('Message ID:', result.messageId);
console.log('Status:', result.status);
console.log('Mobile:', result.mobile);

Common Usage Patterns

Single SMS Delivery

Send a message to a single recipient:

// Basic single SMS
const result = await smsService.send({
  mobile: '0987654321',
  text: 'Your verification code is 123456',
});

// Check delivery status
if (result.status === SMSRequestResult.SUCCESS) {
  console.log('SMS sent successfully!');
  console.log('Message ID:', result.messageId);
  console.log('Recipient:', result.mobile);
} else {
  console.error('SMS delivery failed');
  console.error('Error Code:', result.errorCode);
  console.error('Error Message:', result.errorMessage);
}

Batch Messaging (Same Message to Multiple Recipients)

Send the same message to multiple recipients efficiently:

// Batch send with same message
const results = await smsService.send({
  mobileList: [
    '0987654321',
    '0912345678',
    '0923456789',
    '0934567890'
  ],
  text: 'Important notification: Your order has been shipped!',
});

// Process results
console.log(`Total sent: ${results.length}`);

const successful = results.filter(r => r.status === SMSRequestResult.SUCCESS);
const failed = results.filter(r => r.status === SMSRequestResult.FAILED);

console.log(`Successful: ${successful.length}`);
console.log(`Failed: ${failed.length}`);

// Log failed deliveries
failed.forEach(result => {
  console.error(`Failed to send to ${result.mobile}:`, result.errorMessage);
});

Multi-Target Messaging (Different Messages)

Send different messages to different recipients:

// Send different messages to each recipient
const results = await smsService.send([
  {
    mobile: '0987654321',
    text: 'Dear John, your appointment is confirmed for tomorrow at 10 AM.',
  },
  {
    mobile: '0912345678',
    text: 'Hi Mary, your package will arrive today between 2-4 PM.',
  },
  {
    mobile: '0923456789',
    text: 'Hello Mike, thank you for your purchase! Your receipt is attached.',
  },
]);

// Check each result
results.forEach((result, index) => {
  console.log(`Message ${index + 1} to ${result.mobile}: ${result.status}`);

  if (result.status === SMSRequestResult.SUCCESS) {
    console.log(`  Message ID: ${result.messageId}`);
  } else {
    console.error(`  Error: ${result.errorMessage}`);
  }
});

Taiwan Mobile Number Handling

Automatic Number Normalization

The adapter automatically normalizes Taiwan mobile numbers:

// All these formats are normalized to 0987654321
const validFormats = [
  '0987654321',       // Standard format
  '0987-654-321',     // With dashes
  '+886987654321',    // International format
  '886987654321',     // International without +
  '+886-987654321',   // International with dash after country code
];

// All will be normalized and send successfully
for (const number of validFormats) {
  const result = await smsService.send({
    mobile: number,
    text: 'Test message',
  });

  // Result.mobile will always be '0987654321'
  console.log('Normalized number:', result.mobile);
}

Number Validation

Validate Taiwan mobile numbers with strict mode:

// Enable Taiwan-only validation
const strictService = new SMSServiceEvery8D({
  username: process.env.EVERY8D_USERNAME!,
  password: process.env.EVERY8D_PASSWORD!,
  onlyTaiwanMobileNumber: true, // Enable strict validation
});

try {
  // This will work - valid Taiwan mobile number
  await strictService.send({
    mobile: '0987654321', // Taiwan number (09xxxxxxxx)
    text: 'Valid Taiwan number',
  });

  // This will throw error - international number
  await strictService.send({
    mobile: '+1234567890', // Non-Taiwan number
    text: 'Invalid for Taiwan-only mode',
  });
} catch (error) {
  console.error('Number validation error:', error.message);
  // Error: +1234567890 is not taiwan mobile phone (`onlyTaiwanMobileNumber` option is true)
}

// Taiwan number format requirements:
// - Must start with 09 (or +8869, 8869)
// - Total 10 digits after country code
// - Examples: 0912345678, 0923456789, 0987654321

International Numbers

Send to international numbers when validation is disabled:

// Allow international numbers
const globalService = new SMSServiceEvery8D({
  username: process.env.EVERY8D_USERNAME!,
  password: process.env.EVERY8D_PASSWORD!,
  onlyTaiwanMobileNumber: false, // Default: allow all numbers
});

// Send to various countries
const results = await globalService.send([
  { mobile: '0987654321', text: 'Taiwan message' },        // Taiwan
  { mobile: '+85298765432', text: 'Hong Kong message' },   // Hong Kong
  { mobile: '+60123456789', text: 'Malaysia message' },    // Malaysia
  { mobile: '+6591234567', text: 'Singapore message' },    // Singapore
]);

// Note: Check with Every8D for international coverage and rates

Integration Examples

E-commerce Order Notifications

import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d';
import { SMSRequestResult } from '@rytass/sms';

class OrderNotificationService {
  private smsService: SMSServiceEvery8D;

  constructor() {
    this.smsService = new SMSServiceEvery8D({
      username: process.env.EVERY8D_USERNAME!,
      password: process.env.EVERY8D_PASSWORD!,
      onlyTaiwanMobileNumber: true,
    });
  }

  /**
   * Send order confirmation SMS
   * @param order - Order object with customer details
   * @returns SMS delivery result
   */
  async sendOrderConfirmation(order: {
    id: string;
    customerPhone: string;
    total: number;
    deliveryDate: string;
    trackingUrl: string;
  }): Promise<boolean> {
    const message = `
訂單已確認!
訂單編號:${order.id}
金額:NT$${order.total}
預計送達:${order.deliveryDate}
追蹤連結:${order.trackingUrl}
    `.trim();

    const result = await this.smsService.send({
      mobile: order.customerPhone,
      text: message,
    });

    return result.status === SMSRequestResult.SUCCESS;
  }

  /**
   * Send shipping notification
   * @param order - Order object with tracking number
   * @returns SMS delivery result
   */
  async sendShippingNotification(order: {
    id: string;
    customerPhone: string;
    trackingNumber: string;
  }): Promise<boolean> {
    const message = `您的訂單 #${order.id} 已出貨!追蹤號碼:${order.trackingNumber}`;

    const result = await this.smsService.send({
      mobile: order.customerPhone,
      text: message,
    });

    if (result.status === SMSRequestResult.FAILED) {
      console.error('Failed to send shipping notification:', {
        orderId: order.id,
        errorCode: result.errorCode,
        errorMessage: result.errorMessage,
      });
    }

    return result.status === SMSRequestResult.SUCCESS;
  }

  /**
   * Send delivery notification to multiple orders
   * @param orders - Array of orders ready for delivery
   * @returns Delivery statistics
   */
  async sendBulkDeliveryNotifications(orders: Array<{
    id: string;
    customerPhone: string;
  }>): Promise<{
    total: number;
    successful: number;
    failed: number;
  }> {
    const results = await this.smsService.send(
      orders.map(order => ({
        mobile: order.customerPhone,
        text: `您的訂單 #${order.id} 將於今日送達,請留意收件。`,
      }))
    );

    const successful = results.filter(r => r.status === SMSRequestResult.SUCCESS).length;
    const failed = results.filter(r => r.status === SMSRequestResult.FAILED).length;

    return {
      total: results.length,
      successful,
      failed,
    };
  }
}

Authentication & Verification

import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d';
import { SMSRequestResult } from '@rytass/sms';

class AuthSMSService {
  private smsService: SMSServiceEvery8D;

  constructor() {
    this.smsService = new SMSServiceEvery8D({
      username: process.env.EVERY8D_USERNAME!,
      password: process.env.EVERY8D_PASSWORD!,
      onlyTaiwanMobileNumber: true,
    });
  }

  /**
   * Generate and send verification code
   * @param phoneNumber - Recipient phone number
   * @returns Verification code (store in cache/database)
   */
  async sendVerificationCode(phoneNumber: string): Promise<string> {
    // Generate 6-digit code
    const code = Math.random().toString().slice(-6);

    const message = `您的驗證碼是 ${code},10 分鐘內有效。請勿將此驗證碼告知他人。`;

    const result = await this.smsService.send({
      mobile: phoneNumber,
      text: message,
    });

    if (result.status === SMSRequestResult.SUCCESS) {
      // Store code in Redis/cache with 10-minute expiration
      await this.storeVerificationCode(phoneNumber, code, 600);
      console.log('Verification code sent:', result.messageId);
      return code;
    } else {
      throw new Error(`SMS delivery failed: ${result.errorMessage}`);
    }
  }

  /**
   * Send 2FA code for login
   * @param phoneNumber - User's phone number
   * @param serviceName - Name of the service for branding
   * @returns void
   */
  async send2FACode(
    phoneNumber: string,
    serviceName: string
  ): Promise<void> {
    const code = Math.random().toString().slice(-6);

    const message = `${serviceName} 登入驗證碼:${code},5 分鐘內有效。`;

    const result = await this.smsService.send({
      mobile: phoneNumber,
      text: message,
    });

    if (result.status === SMSRequestResult.SUCCESS) {
      await this.store2FACode(phoneNumber, code, 300); // 5 minutes
    } else {
      throw new Error(`Failed to send 2FA code: ${result.errorMessage}`);
    }
  }

  /**
   * Send password reset link
   * @param phoneNumber - User's phone number
   * @param resetLink - Password reset URL
   * @returns SMS delivery result
   */
  async sendPasswordResetLink(
    phoneNumber: string,
    resetLink: string
  ): Promise<boolean> {
    const message = `密碼重設連結:${resetLink}。此連結將在 30 分鐘後失效。`;

    const result = await this.smsService.send({
      mobile: phoneNumber,
      text: message,
    });

    return result.status === SMSRequestResult.SUCCESS;
  }

  private async storeVerificationCode(
    phone: string,
    code: string,
    expirationSeconds: number
  ): Promise<void> {
    // Implementation depends on your storage solution
    // Redis, database, or in-memory cache
    // Example with Redis:
    // await redis.setex(`verification:${phone}`, expirationSeconds, code);
  }

  private async store2FACode(
    phone: string,
    code: string,
    expirationSeconds: number
  ): Promise<void> {
    // Store with shorter expiration for 2FA
    // await redis.setex(`2fa:${phone}`, expirationSeconds, code);
  }
}

Marketing Campaign Service

import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d';
import { SMSRequestResult } from '@rytass/sms';

class MarketingCampaignService {
  private smsService: SMSServiceEvery8D;

  constructor() {
    this.smsService = new SMSServiceEvery8D({
      username: process.env.EVERY8D_USERNAME!,
      password: process.env.EVERY8D_PASSWORD!,
      onlyTaiwanMobileNumber: true,
    });
  }

  /**
   * Send promotional campaign to customers
   * @param customers - Customer list with phone numbers
   * @param campaignMessage - Campaign message text
   * @returns Campaign statistics
   */
  async sendPromotionalCampaign(
    customers: Array<{ phoneNumber: string; name: string }>,
    campaignMessage: string
  ): Promise<{
    total: number;
    successful: number;
    failed: number;
    successRate: number;
  }> {
    // Split into batches to avoid rate limiting
    const batchSize = 100;
    const batches: Array<typeof customers> = [];

    for (let i = 0; i < customers.length; i += batchSize) {
      batches.push(customers.slice(i, i + batchSize));
    }

    const allResults = [];

    for (const batch of batches) {
      const phoneNumbers = batch.map(customer => customer.phoneNumber);

      try {
        const batchResult = await this.smsService.send({
          mobileList: phoneNumbers,
          text: campaignMessage,
        });

        allResults.push(...batchResult);

        // Add delay between batches to respect rate limits
        await this.delay(1000); // 1 second delay
      } catch (error) {
        console.error('Batch failed:', error);

        // Add failed entries for this batch
        phoneNumbers.forEach(phone => {
          allResults.push({
            mobile: phone,
            status: SMSRequestResult.FAILED,
            errorMessage: 'Batch processing error',
          });
        });
      }
    }

    return this.analyzeCampaignResults(allResults);
  }

  /**
   * Send personalized messages to customers
   * @param customers - Customer list with personalized data
   * @returns Campaign statistics
   */
  async sendPersonalizedCampaign(
    customers: Array<{
      phoneNumber: string;
      name: string;
      customMessage: string;
    }>
  ): Promise<{
    total: number;
    successful: number;
    failed: number;
    successRate: number;
  }> {
    const results = await this.smsService.send(
      customers.map(customer => ({
        mobile: customer.phoneNumber,
        text: customer.customMessage,
      }))
    );

    return this.analyzeCampaignResults(results);
  }

  /**
   * Delay helper for rate limiting
   * @param ms - Milliseconds to delay
   * @returns Promise that resolves after delay
   */
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  /**
   * Analyze campaign results
   * @param results - Array of SMS send results
   * @returns Campaign statistics
   */
  private analyzeCampaignResults(
    results: Array<{ status: SMSRequestResult; mobile: string }>
  ): {
    total: number;
    successful: number;
    failed: number;
    successRate: number;
  } {
    const successful = results.filter(
      r => r.status === SMSRequestResult.SUCCESS
    ).length;
    const failed = results.filter(
      r => r.status === SMSRequestResult.FAILED
    ).length;

    return {
      total: results.length,
      successful,
      failed,
      successRate: (successful / results.length) * 100,
    };
  }
}

Appointment Reminders

import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d';
import { SMSRequestResult } from '@rytass/sms';

class AppointmentReminderService {
  private smsService: SMSServiceEvery8D;

  constructor() {
    this.smsService = new SMSServiceEvery8D({
      username: process.env.EVERY8D_USERNAME!,
      password: process.env.EVERY8D_PASSWORD!,
      onlyTaiwanMobileNumber: true,
    });
  }

  /**
   * Send appointment reminder
   * @param appointment - Appointment details
   * @returns SMS delivery result
   */
  async sendAppointmentReminder(appointment: {
    customerPhone: string;
    customerName: string;
    date: string;
    time: string;
    location: string;
    doctorName?: string;
  }): Promise<boolean> {
    const message = appointment.doctorName
      ? `親愛的 ${appointment.customerName},提醒您與 ${appointment.doctorName} 醫師的預約:${appointment.date} ${appointment.time},地點:${appointment.location}。請提前 10 分鐘報到。`
      : `親愛的 ${appointment.customerName},提醒您的預約:${appointment.date} ${appointment.time},地點:${appointment.location}。請提前 10 分鐘報到。`;

    const result = await this.smsService.send({
      mobile: appointment.customerPhone,
      text: message,
    });

    return result.status === SMSRequestResult.SUCCESS;
  }

  /**
   * Send batch appointment reminders
   * @param appointments - Array of appointments
   * @returns Statistics of sent reminders
   */
  async sendBatchReminders(
    appointments: Array<{
      customerPhone: string;
      date: string;
      time: string;
    }>
  ): Promise<{
    total: number;
    sent: number;
    failed: number;
  }> {
    const results = await this.smsService.send(
      appointments.map(apt => ({
        mobile: apt.customerPhone,
        text: `預約提醒:${apt.date} ${apt.time}。請準時出席,如需取消請提前通知。`,
      }))
    );

    const sent = results.filter(
      r => r.status === SMSRequestResult.SUCCESS
    ).length;
    const failed = results.filter(
      r => r.status === SMSRequestResult.FAILED
    ).length;

    return {
      total: results.length,
      sent,
      failed,
    };
  }
}

NestJS Integration

Basic Setup

// sms.module.ts
import { Module, Global } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d';

export const SMS_SERVICE = Symbol('SMS_SERVICE');

@Global()
@Module({
  imports: [ConfigModule],
  providers: [
    {
      provide: SMS_SERVICE,
      useFactory: (config: ConfigService) => {
        return new SMSServiceEvery8D({
          username: config.get('EVERY8D_USERNAME')!,
          password: config.get('EVERY8D_PASSWORD')!,
          onlyTaiwanMobileNumber: config.get('EVERY8D_TAIWAN_ONLY') === 'true',
        });
      },
      inject: [ConfigService],
    },
  ],
  exports: [SMS_SERVICE],
})
export class SMSModule {}

Using in Services

// notification.service.ts
import { Injectable, Inject, Logger } from '@nestjs/common';
import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d';
import { SMSRequestResult } from '@rytass/sms';
import { SMS_SERVICE } from './sms.module';

@Injectable()
export class NotificationService {
  private readonly logger = new Logger(NotificationService.name);

  constructor(
    @Inject(SMS_SERVICE)
    private readonly smsService: SMSServiceEvery8D,
  ) {}

  /**
   * Send verification code via SMS
   * @param phoneNumber - Recipient phone number
   * @param code - Verification code
   * @returns Whether SMS was sent successfully
   */
  async sendVerificationCode(
    phoneNumber: string,
    code: string,
  ): Promise<boolean> {
    try {
      const result = await this.smsService.send({
        mobile: phoneNumber,
        text: `您的驗證碼是 ${code},10 分鐘內有效。`,
      });

      if (result.status === SMSRequestResult.SUCCESS) {
        this.logger.log(`Verification code sent to ${phoneNumber}`);
        return true;
      } else {
        this.logger.error(
          `Failed to send verification code to ${phoneNumber}: ${result.errorMessage}`,
        );
        return false;
      }
    } catch (error) {
      this.logger.error('SMS sending error:', error);
      return false;
    }
  }

  /**
   * Send bulk notification
   * @param phoneNumbers - Array of recipient phone numbers
   * @param message - Message to send
   * @returns Statistics of sent messages
   */
  async sendBulkNotification(
    phoneNumbers: string[],
    message: string,
  ): Promise<{
    total: number;
    successful: number;
    failed: number;
  }> {
    try {
      const results = await this.smsService.send({
        mobileList: phoneNumbers,
        text: message,
      });

      const successful = results.filter(
        r => r.status === SMSRequestResult.SUCCESS,
      ).length;
      const failed = results.filter(
        r => r.status === SMSRequestResult.FAILED,
      ).length;

      this.logger.log(
        `Bulk notification sent: ${successful} successful, ${failed} failed`,
      );

      return {
        total: results.length,
        successful,
        failed,
      };
    } catch (error) {
      this.logger.error('Bulk SMS sending error:', error);
      throw error;
    }
  }
}

Usage in Controllers

// notification.controller.ts
import { Controller, Post, Body } from '@nestjs/common';
import { NotificationService } from './notification.service';

@Controller('notifications')
export class NotificationController {
  constructor(private readonly notificationService: NotificationService) {}

  @Post('send-verification')
  async sendVerification(
    @Body() body: { phoneNumber: string; code: string },
  ): Promise<{ success: boolean }> {
    const success = await this.notificationService.sendVerificationCode(
      body.phoneNumber,
      body.code,
    );

    return { success };
  }

  @Post('send-bulk')
  async sendBulk(
    @Body() body: { phoneNumbers: string[]; message: string },
  ): Promise<{
    total: number;
    successful: number;
    failed: number;
  }> {
    return await this.notificationService.sendBulkNotification(
      body.phoneNumbers,
      body.message,
    );
  }
}

Configuration

Environment Variables

# .env
EVERY8D_USERNAME=your_every8d_username
EVERY8D_PASSWORD=your_every8d_password
EVERY8D_BASE_URL=https://api.e8d.tw
EVERY8D_TAIWAN_ONLY=true

TypeScript Configuration

// config.ts
export interface SMSConfig {
  username: string;
  password: string;
  baseUrl?: string;
  onlyTaiwanMobileNumber?: boolean;
}

export const smsConfig: SMSConfig = {
  username: process.env.EVERY8D_USERNAME!,
  password: process.env.EVERY8D_PASSWORD!,
  baseUrl: process.env.EVERY8D_BASE_URL || 'https://api.e8d.tw',
  onlyTaiwanMobileNumber: process.env.EVERY8D_TAIWAN_ONLY === 'true',
};

Error Handling

Handling Delivery Failures

import { SMSServiceEvery8D } from '@rytass/sms-adapter-every8d';
import { SMSRequestResult } from '@rytass/sms';
import { Every8DError } from '@rytass/sms-adapter-every8d';

async function sendWithRetry(
  smsService: SMSServiceEvery8D,
  mobile: string,
  text: string,
  maxRetries: number = 3
): Promise<boolean> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      const result = await smsService.send({ mobile, text });

      if (result.status === SMSRequestResult.SUCCESS) {
        console.log(`SMS sent successfully on attempt ${attempt}`);
        return true;
      }

      // Check error code
      if (result.errorCode === Every8DError.FORMAT_ERROR) {
        console.error('Invalid format - no retry needed');
        return false;
      }

      console.warn(`Attempt ${attempt} failed:`, result.errorMessage);

      if (attempt < maxRetries) {
        // Wait before retry (exponential backoff)
        await new Promise(resolve =>
          setTimeout(resolve, Math.pow(2, attempt) * 1000)
        );
      }
    } catch (error) {
      console.error(`Attempt ${attempt} error:`, error);

      if (attempt === maxRetries) {
        throw error;
      }
    }
  }

  return false;
}

Batch Error Handling

async function sendBatchWithErrorHandling(
  smsService: SMSServiceEvery8D,
  phoneNumbers: string[],
  message: string
): Promise<{
  successful: string[];
  failed: Array<{ phone: string; error: string }>;
}> {
  const results = await smsService.send({
    mobileList: phoneNumbers,
    text: message,
  });

  const successful: string[] = [];
  const failed: Array<{ phone: string; error: string }> = [];

  results.forEach(result => {
    if (result.status === SMSRequestResult.SUCCESS) {
      successful.push(result.mobile);
    } else {
      failed.push({
        phone: result.mobile,
        error: result.errorMessage || 'Unknown error',
      });
    }
  });

  // Log failures
  if (failed.length > 0) {
    console.error('Failed deliveries:', failed);
  }

  return { successful, failed };
}

Best Practices

Message Content

class MessageTemplates {
  /**
   * Verification code template
   * - Keep message under 70 characters for single SMS
   * - Include expiration time
   * - Add security warning
   */
  static verification(code: string): string {
    return `您的驗證碼是 ${code},10 分鐘內有效。請勿將此驗證碼告知他人。`;
  }

  /**
   * Order confirmation template
   * - Include order number for reference
   * - Keep essential info only
   */
  static orderConfirmation(orderNumber: string, amount: number): string {
    return `訂單 ${orderNumber} 已確認。金額:NT$${amount}。感謝您的購買!`;
  }

  /**
   * Appointment reminder template
   * - Include date, time, and location
   * - Add action instructions
   */
  static appointmentReminder(date: string, time: string): string {
    return `預約提醒:${date} ${time}。請提前 10 分鐘報到。如需取消請來電。`;
  }

  /**
   * Marketing campaign template
   * - Include opt-out instructions for compliance
   * - Keep under 70 characters if possible
   */
  static promotion(discount: string): string {
    return `限時優惠!${discount} 折扣活動進行中。查看詳情:[連結]。回 N 取消訂閱。`;
  }
}

Security

// 1. Store credentials securely
// ❌ Don't hardcode credentials
const badService = new SMSServiceEvery8D({
  username: 'myusername',
  password: 'mypassword',
});

// ✅ Use environment variables
const goodService = new SMSServiceEvery8D({
  username: process.env.EVERY8D_USERNAME!,
  password: process.env.EVERY8D_PASSWORD!,
});

// 2. Validate phone numbers before sending
function isValidTaiwanMobile(phone: string): boolean {
  const cleaned = phone.replace(/[^0-9]/g, '').replace(/^886/, '0');
  return /^09\d{8}$/.test(cleaned);
}

// 3. Implement rate limiting
class RateLimitedSMSService {
  private lastSendTime = 0;
  private minInterval = 1000; // 1 second between sends

  async send(smsService: SMSServiceEvery8D, data: any) {
    const now = Date.now();
    const timeSinceLastSend = now - this.lastSendTime;

    if (timeSinceLastSend < this.minInterval) {
      await new Promise(resolve =>
        setTimeout(resolve, this.minInterval - timeSinceLastSend)
      );
    }

    const result = await smsService.send(data);
    this.lastSendTime = Date.now();

    return result;
  }
}

// 4. Log all SMS activities for audit
async function sendWithAuditLog(
  smsService: SMSServiceEvery8D,
  mobile: string,
  text: string,
  userId?: string
): Promise<void> {
  const auditEntry = {
    timestamp: new Date(),
    userId,
    recipient: mobile,
    messageLength: text.length,
    action: 'SMS_SEND',
  };

  try {
    const result = await smsService.send({ mobile, text });

    await logAudit({
      ...auditEntry,
      status: result.status,
      messageId: result.messageId,
    });
  } catch (error) {
    await logAudit({
      ...auditEntry,
      status: 'ERROR',
      error: error.message,
    });
    throw error;
  }
}

async function logAudit(entry: any): Promise<void> {
  // Save to database or logging service
}

Performance

// 1. Batch messages efficiently
// ❌ Don't send one by one
async function inefficientSend(phones: string[], message: string) {
  for (const phone of phones) {
    await smsService.send({ mobile: phone, text: message });
  }
}

// ✅ Use batch send
async function efficientSend(phones: string[], message: string) {
  await smsService.send({ mobileList: phones, text: message });
}

// 2. Handle large batches with chunking
async function sendLargeBatch(
  smsService: SMSServiceEvery8D,
  phones: string[],
  message: string,
  chunkSize: number = 100
): Promise<void> {
  for (let i = 0; i < phones.length; i += chunkSize) {
    const chunk = phones.slice(i, i + chunkSize);

    await smsService.send({
      mobileList: chunk,
      text: message,
    });

    // Add delay between chunks
    if (i + chunkSize < phones.length) {
      await new Promise(resolve => setTimeout(resolve, 1000));
    }
  }
}

Compliance

// 1. Respect user preferences and opt-out requests
interface UserSMSPreference {
  phoneNumber: string;
  optedOut: boolean;
  marketingConsent: boolean;
  transactionalOnly: boolean;
}

async function sendRespectingPreferences(
  smsService: SMSServiceEvery8D,
  user: UserSMSPreference,
  message: string,
  messageType: 'marketing' | 'transactional'
): Promise<boolean> {
  // Check if user opted out completely
  if (user.optedOut) {
    console.log('User opted out - skipping SMS');
    return false;
  }

  // Check marketing consent
  if (messageType === 'marketing' && !user.marketingConsent) {
    console.log('No marketing consent - skipping SMS');
    return false;
  }

  // Send the message
  const result = await smsService.send({
    mobile: user.phoneNumber,
    text: message,
  });

  return result.status === SMSRequestResult.SUCCESS;
}

// 2. Include opt-out instructions in marketing messages
function createMarketingMessage(content: string): string {
  return `${content}\n\n回覆 N 取消訂閱簡訊通知。`;
}

// 3. Maintain do-not-contact list
class DoNotContactList {
  private blockedNumbers = new Set<string>();

  async add(phoneNumber: string): Promise<void> {
    this.blockedNumbers.add(phoneNumber);
    // Persist to database
  }

  async remove(phoneNumber: string): Promise<void> {
    this.blockedNumbers.delete(phoneNumber);
    // Update database
  }

  isBlocked(phoneNumber: string): boolean {
    return this.blockedNumbers.has(phoneNumber);
  }

  async filterAllowed(phoneNumbers: string[]): Promise<string[]> {
    return phoneNumbers.filter(phone => !this.isBlocked(phone));
  }
}

Internal Implementation Details

Empty Target Handling

傳入空陣列或空 mobileList 會拋出錯誤:

// 以下情況會拋出 'No target provided.' 錯誤
await smsService.send([]);  // 空陣列
await smsService.send({ mobileList: [], text: 'Test' });  // 空 mobileList

Batch Message Optimization

相同訊息內容的多個請求會自動合併為單一 API 呼叫,提高發送效率:

// 這三個請求會合併成一個 API 呼叫
const results = await smsService.send([
  { mobile: '0912345678', text: 'Hello' },
  { mobile: '0923456789', text: 'Hello' },  // 相同訊息
  { mobile: '0934567890', text: 'Hello' },  // 相同訊息
]);
// 內部會將 DEST 參數合併為 '0912345678,0923456789,0934567890'

Partial Delivery Failure Handling

當部分發送失敗時,API 回傳 unsend 數量,系統會從列表末端開始標記失敗:

// 假設發送 5 個號碼,API 回傳 unsend=2
// 則前 3 個標記為 SUCCESS,後 2 個標記為 FAILED
const results = await smsService.send({
  mobileList: ['0911...', '0922...', '0933...', '0944...', '0955...'],
  text: 'Test',
});
// results[0..2].status === SMSRequestResult.SUCCESS
// results[3..4].status === SMSRequestResult.FAILED

Troubleshooting

Common Issues

1. Authentication Failure

Symptoms:

  • All messages fail to send
  • Error message about credentials

Solutions:

// Check credentials
console.log('Username:', process.env.EVERY8D_USERNAME);
console.log('Password length:', process.env.EVERY8D_PASSWORD?.length);

// Ensure no extra spaces in .env
EVERY8D_USERNAME=your_username  # ❌ Trailing space
EVERY8D_USERNAME=your_username   # ✅ No space

// Test with minimal code
const testService = new SMSServiceEvery8D({
  username: 'YOUR_USERNAME',
  password: 'YOUR_PASSWORD',
});

const result = await testService.send({
  mobile: '0987654321',
  text: 'Test',
});

console.log('Result:', result);

2. Invalid Phone Number Format

Symptoms:

  • FORMAT_ERROR (-306) error code
  • Message fails for specific numbers

Solutions:

// Enable Taiwan-only validation
const service = new SMSServiceEvery8D({
  username: process.env.EVERY8D_USERNAME!,
  password: process.env.EVERY8D_PASSWORD!,
  onlyTaiwanMobileNumber: true,
});

// Validate before sending
function validateTaiwanMobile(phone: string): boolean {
  const normalized = phone.replace(/[^0-9]/g, '').replace(/^886/, '0');
  return /^09\d{8}$/.test(normalized);
}

if (!validateTaiwanMobile(phoneNumber)) {
  console.error('Invalid Taiwan mobile number:', phoneNumber);
}

3. Message Not Delivered

Symptoms:

  • Status shows SUCCESS but message not received
  • Delays in delivery

Solutions:

// 1. Check message content
// - Avoid special characters that might cause issues
// - Keep message under 70 characters for single SMS

// 2. Verify phone number is active
// - Test with your own phone first

// 3. Check account balance
// - Contact Every8D to verify account status

// 4. Log message ID for tracking
const result = await smsService.send({ mobile, text });
console.log('Message ID for tracking:', result.messageId);

API Reference

SMSServiceEvery8D

Constructor Options:

Property Type Required Default Description
username string Yes - Every8D account username
password string Yes - Every8D account password
baseUrl string No 'https://api.e8d.tw' API endpoint URL
onlyTaiwanMobileNumber boolean No false Restrict to Taiwan mobile numbers only

Methods:

// Send single SMS
send(request: Every8DSMSRequest): Promise<Every8DSMSSendResponse>

// Send multiple SMS with different messages
send(requests: Every8DSMSRequest[]): Promise<Every8DSMSSendResponse[]>

// Send same message to multiple recipients
send(request: Every8DSMSMultiTargetRequest): Promise<Every8DSMSSendResponse[]>

Request Types

interface Every8DSMSRequest {
  mobile: string;  // Phone number (will be normalized)
  text: string;    // Message content
}

interface Every8DSMSMultiTargetRequest {
  mobileList: string[];  // Array of phone numbers
  text: string;          // Message content (same for all)
}

Response Types

interface Every8DSMSSendResponse {
  messageId?: string;              // Message ID for tracking
  status: SMSRequestResult;        // SUCCESS or FAILED
  mobile: string;                  // Normalized phone number
  errorMessage?: string;           // Error message if failed
  errorCode?: Every8DError;        // Error code if failed
}

enum SMSRequestResult {
  SUCCESS = 'SUCCESS',
  FAILED = 'FAILED',
}

enum Every8DError {
  FORMAT_ERROR = -306,
  UNKNOWN = -99,
}

Advanced Topics

For creating new SMS adapters or extending functionality, see the SMS Development Guide.

Weekly Installs
6
Repository
rytass/utils
GitHub Stars
6
First Seen
Feb 5, 2026
Installed on
amp6
github-copilot6
replit6
codex6
kimi-cli6
gemini-cli6