sms-adapters
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 recipientMultiTargetRequest- Same message to multiple recipients
Response Status:
SUCCESS- Message sent successfullyFAILED- 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.
More from rytass/utils
wms-module
|
24logistics-development
|
13logistics-adapters
|
12invoice-adapters
Taiwan e-invoice integration (台灣電子發票整合). Use when working with ECPay (綠界), EZPay (藍新), BankPro (金財通), or Amego (光貿) invoice services. Covers issuing invoices (開立發票), voiding (作廢), allowances (折讓), and querying invoice data.
8payment-adapters
Taiwan payment integration (台灣支付整合). Use when working with ECPay (綠界), NewebPay (藍新), HwaNan Bank (華南銀行), CTBC (中信), iCash Pay, or Happy Card payment services. Covers credit card (信用卡), virtual account (虛擬帳號), ATM, CVS payment (超商代碼/條碼), card binding (卡片綁定), installments (分期付款), recurring payments (訂閱付款), and NestJS integration.
7member-module
|
7