Просмотр исходного кода

add specs for refactor-mailer-service

Yuki Takei 1 месяц назад
Родитель
Сommit
d70926a257

+ 759 - 0
.kiro/specs/refactor-mailer-service/design.md

@@ -0,0 +1,759 @@
+# Technical Design: MailService Refactoring
+
+## Overview
+
+This refactoring modernizes the MailService implementation to improve code organization, maintainability, and type safety through modular architecture and compile-time validation.
+
+**Purpose**: This feature delivers improved code maintainability and compile-time type safety to developers working with email functionality. The refactoring separates transmission methods (SMTP, SES, OAuth2) into independent modules and leverages TypeScript's type system to prevent credential-related runtime errors.
+
+**Users**: Developers maintaining or extending GROWI's email functionality will utilize this for easier testing, debugging, and feature additions.
+
+**Impact**: Changes the current monolithic MailService implementation (~408 lines) by restructuring into a modular architecture with separate transport modules, while maintaining full backward compatibility with existing code.
+
+### Goals
+
+- Organize mail-related files into feature-based directory structure (`mail/`)
+- Separate email transmission methods (SMTP, SES, OAuth2) into independent, testable modules
+- Replace runtime falsy checks with TypeScript type guards for OAuth2 credentials
+- Maintain 100% backward compatibility with existing MailService public API
+- Improve type safety using @growi/core's NonBlankString branded types
+
+### Non-Goals
+
+- Changing MailService public API or behavior (beyond internal organization)
+- Adding new transmission methods or email features
+- Modifying retry logic, error handling, or failed email storage mechanisms
+- Migrating away from nodemailer library
+- Implementing OAuth2 token refresh logic improvements (deferred to future iteration)
+
+## Architecture
+
+### Existing Architecture Analysis
+
+**Current Implementation**:
+- Single file: `src/server/service/mail.ts` (~408 lines)
+- MailService class with three inline transport creation methods:
+  - `createSMTPClient()` - SMTP transport with username/password auth
+  - `createSESClient()` - AWS SES transport with IAM credentials
+  - `createOAuth2Client()` - Gmail OAuth2 transport with refresh tokens
+- Implements `S2sMessageHandlable` interface for cross-service configuration synchronization
+- Uses runtime falsy checks for credential validation: `!clientId || !clientSecret || !refreshToken || !user`
+- Single test file: `src/server/service/mail.spec.ts`
+
+**Current Constraints**:
+- Must maintain `S2sMessageHandlable` interface implementation
+- Must preserve `constructor(crowi: Crowi)` signature
+- Must continue to work with existing config keys (`mail:transmissionMethod`, `mail:oauth2ClientId`, etc.)
+- Import path `~/server/service/mail` must remain valid
+
+**Integration Points**:
+- Config API (`src/server/routes/apiv3/app-settings/index.ts`) - reads/updates mail settings
+- Global Notification Service (`src/server/service/global-notification/index.ts`) - sends notification emails
+- Crowi initialization - instantiates MailService during server startup
+
+### Architecture Pattern & Boundary Map
+
+```mermaid
+graph TB
+    subgraph "External Dependencies"
+        Config[ConfigManager]
+        NM[nodemailer]
+        Core[@growi/core]
+    end
+
+    subgraph "mail/ Module"
+        MS[MailService]
+        SMTP[smtp.ts]
+        SES[ses.ts]
+        OAuth2[oauth2.ts]
+        Types[types.ts]
+        Index[index.ts]
+    end
+
+    subgraph "Consumers"
+        API[Config API]
+        Notif[Global Notification]
+    end
+
+    Config --> MS
+    MS --> SMTP
+    MS --> SES
+    MS --> OAuth2
+    SMTP --> NM
+    SES --> NM
+    OAuth2 --> NM
+    OAuth2 --> Core
+    Index --> MS
+    API --> Index
+    Notif --> Index
+```
+
+**Architecture Integration**:
+- **Selected pattern**: Factory Functions - each transport module exports a pure function that creates transport instances
+- **Domain boundaries**:
+  - `MailService` - Orchestration layer, S2S messaging, initialization coordination
+  - `smtp.ts`, `ses.ts`, `oauth2.ts` - Transport-specific credential handling and client creation
+  - `types.ts` - Shared type definitions (MailConfig, EmailConfig, SendResult, StrictOAuth2Options)
+- **Existing patterns preserved**:
+  - Service-based architecture (MailService as singleton in Crowi context)
+  - ConfigManager-based configuration retrieval
+  - S2sMessageHandlable interface for distributed systems
+  - Named exports (except barrel file default export for compatibility)
+- **New components rationale**:
+  - Transport modules: Single responsibility, independently testable, clear failure modes
+  - Barrel export: Maintains backward compatibility while enabling internal modularity
+- **Steering compliance**:
+  - Feature-based directory structure (GROWI monorepo standard)
+  - Immutability (no object mutation in transport creation)
+  - Co-located tests (each module has adjacent .spec.ts file)
+
+### Technology Stack
+
+| Layer | Choice / Version | Role in Feature | Notes |
+|-------|------------------|-----------------|-------|
+| Backend / Services | Node.js 18+ | Runtime environment | No change from existing |
+| Type System | TypeScript 5.x | Compile-time validation | Leverage branded types (NonBlankString) |
+| Email Library | nodemailer 6.9.15 | SMTP/OAuth2 transport | Existing dependency, no upgrade |
+| Type Definitions | @types/nodemailer@6.4.22 | nodemailer TypeScript types | **New dependency** - provides base types for extension |
+| Core Types | @growi/core (NonBlankString) | Branded type validation | Existing infrastructure, used for strict credential types |
+| Testing | Vitest | Unit testing framework | No change from existing test infrastructure |
+
+**Rationale**: @types/nodemailer provides foundational type safety for nodemailer API, while StrictOAuth2Options extends these with NonBlankString for compile-time empty string prevention (see research.md for type compatibility analysis).
+
+## Requirements Traceability
+
+| Requirement | Summary | Components | Interfaces | Flows |
+|-------------|---------|------------|------------|-------|
+| 1.1 | MailService at mail/mail.ts | MailService | - | - |
+| 1.2 | Test file at mail/mail.spec.ts | MailService | - | - |
+| 1.3 | Barrel file at mail/index.ts | index.ts | Exports MailService | - |
+| 1.4 | Import from ~/server/service/mail | index.ts | Default export | - |
+| 1.5 | Legacy import path works | index.ts | Alias resolution | - |
+| 2.1 | SMTP module at mail/smtp.ts | SmtpTransport | createSMTPClient() | - |
+| 2.2 | SES module at mail/ses.ts | SesTransport | createSESClient() | - |
+| 2.3 | OAuth2 module at mail/oauth2.ts | OAuth2Transport | createOAuth2Client() | - |
+| 2.4 | Delegate transport creation | MailService.initialize() | Transport factories | Initialization |
+| 2.5 | Transport function signature | All transport modules | create[Transport]Client | - |
+| 2.6 | Return null for incomplete credentials | All transport modules | - | Error path |
+| 2.7 | Follow immutability conventions | All modules | - | - |
+| 3.1-3.3 | Type infrastructure (@types/nodemailer, StrictOAuth2Options) | types.ts, package.json | Type definitions | - |
+| 3.4-3.7 | Runtime validation (toNonBlankStringOrUndefined, type guards) | oauth2.ts | - | Credential validation |
+| 3.8-3.12 | Compile-time safety (type errors, no any, strict mode) | StrictOAuth2Options, oauth2.ts | Type constraints | Build-time checks |
+| 4.1-4.6 | Backward compatibility | MailService | All existing methods/properties | All existing flows |
+| 5.1-5.7 | Co-located testing | All spec files | Test interfaces | Test execution |
+
+## Implementation Order
+
+The refactoring follows a three-phase approach that ensures type safety from the beginning and maintains a working codebase at each step:
+
+### Phase 1: Type Safety Foundation
+**Requirements**: 3.1-3.3, 3.8-3.12
+
+**Actions**:
+1. Install `@types/nodemailer@6.4.22` as devDependency
+2. Create `src/server/service/mail/types.ts` with:
+   - `StrictOAuth2Options` type definition (with NonBlankString)
+   - Shared types: `MailConfig`, `EmailConfig`, `SendResult`
+3. Verify type compilation with `pnpm run lint:typecheck`
+4. Verify existing tests still pass with `pnpm run test`
+
+**Checkpoint**: ✅ Type definitions exist, existing code unchanged and functional
+
+### Phase 2: Module Extraction
+**Requirements**: 2.1-2.7, 3.4-3.7
+
+**Actions**:
+1. Extract SMTP logic to `smtp.ts` + create `smtp.spec.ts`
+2. Extract SES logic to `ses.ts` + create `ses.spec.ts`
+3. Extract OAuth2 logic to `oauth2.ts` (using NonBlankString) + create `oauth2.spec.ts`
+4. Each module implements `create[Transport]Client(configManager): Transporter | null`
+5. Tests verify null return for incomplete credentials
+
+**Checkpoint**: ✅ Transport modules exist with type-safe implementations, tested in isolation
+
+### Phase 3: Integration & Barrel Export
+**Requirements**: 1.1-1.5, 2.4, 4.1-4.6, 5.1-5.7
+
+**Actions**:
+1. Move `mail.ts` to `mail/mail.ts`
+2. Update MailService to import and delegate to transport modules
+3. Remove inline `createSMTPClient()`, `createSESClient()`, `createOAuth2Client()` methods
+4. Move `mail.spec.ts` to `mail/mail.spec.ts` and update imports
+5. Create `mail/index.ts` barrel export (default export for backward compatibility)
+6. Run full test suite to verify backward compatibility
+
+**Checkpoint**: ✅ Refactoring complete, all tests pass, backward compatibility verified
+
+**Rationale**: This order ensures:
+- Type definitions are available before writing any transport code
+- Each transport module is type-safe from the moment it's created
+- No "add types later" technical debt
+- Continuous verification at each checkpoint
+
+## Components and Interfaces
+
+### Summary Table
+
+| Component | Domain/Layer | Intent | Req Coverage | Key Dependencies (P0/P1) | Contracts |
+|-----------|--------------|--------|--------------|--------------------------|-----------|
+| MailService | Service | Email orchestration and S2S coordination | 1.1-1.5, 2.4, 4.1-4.6 | ConfigManager (P0), Transport modules (P0) | Service, State |
+| SmtpTransport | Transport | SMTP client creation with auth | 2.1, 2.5-2.7 | nodemailer (P0), ConfigManager (P0) | Service |
+| SesTransport | Transport | AWS SES client creation | 2.2, 2.5-2.7 | nodemailer (P0), ConfigManager (P0) | Service |
+| OAuth2Transport | Transport | OAuth2 client with type-safe credentials | 2.3, 2.5-2.7, 3.1-3.12 | nodemailer (P0), @growi/core (P0), ConfigManager (P0) | Service |
+| types.ts | Types | Shared type definitions | 3.1-3.3 | @growi/core (P0), @types/nodemailer (P1) | Type definitions |
+| index.ts | Barrel | Public API export | 1.3-1.5 | MailService (P0) | Export contract |
+
+### Service Layer
+
+#### MailService
+
+| Field | Detail |
+|-------|--------|
+| Intent | Orchestrate email sending, coordinate transport initialization, handle S2S configuration updates |
+| Requirements | 1.1-1.5, 2.4, 4.1-4.6 |
+
+**Responsibilities & Constraints**
+- Orchestrate transport selection based on `mail:transmissionMethod` config value
+- Maintain existing public API: `send()`, `initialize()`, `publishUpdatedMessage()`
+- Implement `S2sMessageHandlable` interface for distributed configuration synchronization
+- Delegate transport creation to specialized modules
+- Preserve retry logic, error handling, and failed email storage behavior
+
+**Dependencies**
+- Inbound: Config API, Global Notification Service — email sending requests (P0)
+- Outbound: SmtpTransport, SesTransport, OAuth2Transport — transport creation (P0)
+- Outbound: ConfigManager — configuration retrieval (P0)
+- External: nodemailer — email transmission (P0)
+
+**Contracts**: Service [x] / State [x]
+
+##### Service Interface
+
+```typescript
+class MailService implements S2sMessageHandlable {
+  // Public properties (existing)
+  isMailerSetup: boolean;
+  mailer: any;  // nodemailer.Transporter
+  mailConfig: MailConfig;
+
+  // Constructor (existing signature)
+  constructor(crowi: Crowi);
+
+  // Public methods (existing)
+  initialize(): void;
+  send(config: EmailConfig): Promise<SendResult>;
+  publishUpdatedMessage(): Promise<void>;
+
+  // S2sMessageHandlable interface (existing)
+  shouldHandleS2sMessage(s2sMessage: S2sMessage): boolean;
+  handleS2sMessage(s2sMessage: S2sMessage): Promise<void>;
+
+  // Existing helper methods (unchanged)
+  sendWithRetry(config: EmailConfig, maxRetries?: number): Promise<SendResult>;
+  storeFailedEmail(config: EmailConfig, error: Error & { code?: string }): Promise<void>;
+  maskCredential(credential: string): string;
+  exponentialBackoff(attempt: number): Promise<void>;
+}
+```
+
+**Preconditions**:
+- Crowi instance must be initialized with ConfigManager, appService, s2sMessagingService
+- Config must include `mail:from` and valid `mail:transmissionMethod`
+
+**Postconditions**:
+- `isMailerSetup` reflects successful transport initialization
+- `mailer` contains nodemailer transport instance or null
+
+**Invariants**:
+- If `isMailerSetup === true`, then `mailer !== null`
+- `send()` method preserves existing behavior (retry logic, error handling, failed email storage)
+
+##### State Management
+
+**State model**:
+- `isMailerSetup: boolean` - Flag indicating successful transport initialization
+- `mailer: any` - Nodemailer transport instance (SMTP/SES/OAuth2)
+- `mailConfig: MailConfig` - Current mail configuration (from, subject)
+- `lastLoadedAt: Date` - Timestamp of last configuration load (for S2S sync)
+
+**Persistence & consistency**:
+- No direct persistence (reads from ConfigManager)
+- S2S messaging ensures configuration consistency across distributed instances
+
+**Concurrency strategy**:
+- `initialize()` called synchronously during construction and S2S message handling
+- No locking required (single-threaded Node.js event loop)
+
+**Implementation Notes**
+- **Integration**: Delegates transport creation to `createSMTPClient()`, `createSESClient()`, `createOAuth2Client()` factory functions
+- **Validation**: Transport modules handle credential validation; MailService handles null return (transport creation failure)
+- **Risks**: Existing consumers must not be affected by internal restructuring; integration tests verify import paths and method signatures
+
+### Transport Layer
+
+#### SmtpTransport
+
+| Field | Detail |
+|-------|--------|
+| Intent | Create nodemailer SMTP transport with username/password authentication |
+| Requirements | 2.1, 2.5-2.7 |
+
+**Responsibilities & Constraints**
+- Read SMTP configuration from ConfigManager (`mail:smtpHost`, `mail:smtpPort`, `mail:smtpUser`, `mail:smtpPassword`)
+- Return `null` if required credentials are missing
+- Configure TLS settings (`rejectUnauthorized: false` for self-signed certificates)
+
+**Dependencies**
+- Inbound: MailService.initialize() — transport creation request (P0)
+- Outbound: ConfigManager — config retrieval (P0)
+- External: nodemailer — transport instance creation (P0)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+/**
+ * Creates an SMTP transport client for email sending.
+ *
+ * @param configManager - Configuration manager instance
+ * @param option - Optional SMTP configuration (for testing)
+ * @returns nodemailer Transporter instance, or null if credentials incomplete
+ *
+ * @remarks
+ * Config keys required: mail:smtpHost, mail:smtpPort
+ * Config keys optional: mail:smtpUser, mail:smtpPassword (auth)
+ */
+export function createSMTPClient(
+  configManager: IConfigManagerForApp,
+  option?: SMTPTransport.Options
+): Transporter | null;
+```
+
+**Preconditions**:
+- ConfigManager contains `mail:smtpHost` and `mail:smtpPort`
+
+**Postconditions**:
+- Returns nodemailer SMTP transport instance with configured auth (if credentials present)
+- Returns `null` if host or port missing
+
+**Implementation Notes**
+- **Integration**: Called by MailService.initialize() when `mail:transmissionMethod === 'smtp'`
+- **Validation**: Checks `host == null || port == null` for null return
+- **Risks**: TLS configuration (`rejectUnauthorized: false`) may pose security risk in production
+
+#### SesTransport
+
+| Field | Detail |
+|-------|--------|
+| Intent | Create nodemailer SES transport with AWS IAM credentials |
+| Requirements | 2.2, 2.5-2.7 |
+
+**Responsibilities & Constraints**
+- Read SES configuration from ConfigManager (`mail:sesAccessKeyId`, `mail:sesSecretAccessKey`)
+- Return `null` if required credentials are missing
+- Use nodemailer-ses-transport adapter
+
+**Dependencies**
+- Inbound: MailService.initialize() — transport creation request (P0)
+- Outbound: ConfigManager — config retrieval (P0)
+- External: nodemailer, nodemailer-ses-transport — transport instance creation (P0)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+/**
+ * Creates an AWS SES transport client for email sending.
+ *
+ * @param configManager - Configuration manager instance
+ * @param option - Optional SES configuration (for testing)
+ * @returns nodemailer Transporter instance, or null if credentials incomplete
+ *
+ * @remarks
+ * Config keys required: mail:sesAccessKeyId, mail:sesSecretAccessKey
+ */
+export function createSESClient(
+  configManager: IConfigManagerForApp,
+  option?: { accessKeyId: string; secretAccessKey: string }
+): Transporter | null;
+```
+
+**Preconditions**:
+- ConfigManager contains `mail:sesAccessKeyId` and `mail:sesSecretAccessKey`
+
+**Postconditions**:
+- Returns nodemailer SES transport instance with AWS credentials
+- Returns `null` if access key or secret key missing
+
+**Implementation Notes**
+- **Integration**: Called by MailService.initialize() when `mail:transmissionMethod === 'ses'`
+- **Validation**: Checks `accessKeyId == null || secretAccessKey == null` for null return
+- **Risks**: Requires nodemailer-ses-transport dependency (existing)
+
+#### OAuth2Transport
+
+| Field | Detail |
+|-------|--------|
+| Intent | Create nodemailer OAuth2 transport with type-safe, non-blank credentials |
+| Requirements | 2.3, 2.5-2.7, 3.1-3.12 |
+
+**Responsibilities & Constraints**
+- Read OAuth2 configuration from ConfigManager (`mail:oauth2User`, `mail:oauth2ClientId`, `mail:oauth2ClientSecret`, `mail:oauth2RefreshToken`)
+- Convert config values to `NonBlankString` using `toNonBlankStringOrUndefined()`
+- Return `null` if any credential is `undefined` after conversion
+- Construct `StrictOAuth2Options` with type-safe credentials
+- Log warning for incomplete credentials
+
+**Dependencies**
+- Inbound: MailService.initialize() — transport creation request (P0)
+- Outbound: ConfigManager — config retrieval (P0)
+- Outbound: @growi/core — NonBlankString helpers (P0)
+- External: nodemailer — transport instance creation (P0)
+
+**Contracts**: Service [x]
+
+##### Service Interface
+
+```typescript
+/**
+ * Creates a Gmail OAuth2 transport client with type-safe credentials.
+ *
+ * @param configManager - Configuration manager instance
+ * @param option - Optional OAuth2 configuration (for testing)
+ * @returns nodemailer Transporter instance, or null if credentials incomplete
+ *
+ * @remarks
+ * Config keys required: mail:oauth2User, mail:oauth2ClientId,
+ *                       mail:oauth2ClientSecret, mail:oauth2RefreshToken
+ *
+ * All credentials must be non-blank strings (length > 0 after trim).
+ * Uses NonBlankString branded type to prevent empty string credentials at compile time.
+ *
+ * @example
+ * ```typescript
+ * const transport = createOAuth2Client(configManager);
+ * if (transport === null) {
+ *   logger.warn('OAuth2 credentials incomplete');
+ * }
+ * ```
+ */
+export function createOAuth2Client(
+  configManager: IConfigManagerForApp,
+  option?: SMTPTransport.Options
+): Transporter | null;
+```
+
+**Preconditions**:
+- ConfigManager contains all four OAuth2 credential keys
+- Credentials are non-blank strings (validated at compile time via NonBlankString type)
+
+**Postconditions**:
+- Returns nodemailer OAuth2 transport instance with Gmail service configuration
+- Returns `null` if any credential is blank or missing
+- Logs warning when returning null
+
+**Invariants**:
+- If function returns non-null transport, all credentials are NonBlankString (guaranteed by type system)
+
+**Implementation Notes**
+- **Integration**: Called by MailService.initialize() when `mail:transmissionMethod === 'oauth2'`
+- **Validation**: Uses `toNonBlankStringOrUndefined()` for conversion, then type guards (`=== undefined`) for null check
+- **Risks**: Type safety depends on correct usage of NonBlankString helpers; runtime validation removed in favor of type guards
+
+### Type Layer
+
+#### types.ts
+
+| Field | Detail |
+|-------|--------|
+| Intent | Provide shared type definitions for mail module |
+| Requirements | 3.1-3.3 |
+
+**Responsibilities & Constraints**
+- Define `StrictOAuth2Options` type with NonBlankString credential fields
+- Export shared types: MailConfig, EmailConfig, SendResult
+- Maintain compatibility with nodemailer's SMTPTransport.Options
+
+**Dependencies**
+- Outbound: @growi/core — NonBlankString type (P0)
+- External: @types/nodemailer — SMTPTransport types (P1)
+
+**Contracts**: Type definitions
+
+##### Type Definitions
+
+```typescript
+import type { NonBlankString } from '@growi/core/dist/interfaces';
+import type SMTPTransport from 'nodemailer/lib/smtp-transport';
+
+/**
+ * Type-safe OAuth2 configuration with non-blank string validation.
+ *
+ * This type is stricter than nodemailer's default XOAuth2.Options, which allows
+ * empty strings. By using NonBlankString, we prevent empty credentials at compile time,
+ * matching nodemailer's runtime falsy checks (`!this.options.refreshToken`).
+ *
+ * @see https://github.com/nodemailer/nodemailer/blob/master/lib/xoauth2/index.js
+ */
+export type StrictOAuth2Options = {
+  service: 'gmail';
+  auth: {
+    type: 'OAuth2';
+    user: NonBlankString;
+    clientId: NonBlankString;
+    clientSecret: NonBlankString;
+    refreshToken: NonBlankString;
+  };
+};
+
+export type MailConfig = {
+  to?: string;
+  from?: string;
+  text?: string;
+  subject?: string;
+};
+
+export type EmailConfig = {
+  to: string;
+  from?: string;
+  subject?: string;
+  text?: string;
+  template?: string;
+  vars?: Record<string, unknown>;
+};
+
+export type SendResult = {
+  messageId: string;
+  response: string;
+  envelope: {
+    from: string;
+    to: string[];
+  };
+};
+
+// Type assertion: StrictOAuth2Options is compatible with SMTPTransport.Options
+// This ensures our strict type can be passed to nodemailer.createTransport()
+declare const _typeCheck: SMTPTransport.Options extends StrictOAuth2Options ? never : 'Type mismatch';
+```
+
+**Implementation Notes**
+- **Integration**: Imported by oauth2.ts and mail.ts for type-safe credential handling
+- **Validation**: Type compatibility verified via `declare const _typeCheck` assertion
+- **Risks**: Future nodemailer type changes may require updates to StrictOAuth2Options
+
+### Export Layer
+
+#### index.ts
+
+| Field | Detail |
+|-------|--------|
+| Intent | Provide backward-compatible barrel export for MailService |
+| Requirements | 1.3-1.5 |
+
+**Responsibilities & Constraints**
+- Export MailService as default export (backward compatibility)
+- Export types as named exports (future extensibility)
+
+**Contracts**: Export contract
+
+```typescript
+/**
+ * Mail service barrel export.
+ *
+ * Maintains backward compatibility with existing import pattern:
+ * `import MailService from '~/server/service/mail'`
+ */
+export { default } from './mail';
+export type { MailConfig, EmailConfig, SendResult, StrictOAuth2Options } from './types';
+```
+
+## Error Handling
+
+### Error Strategy
+
+The refactored MailService maintains existing error handling behavior:
+- Transport creation failures return `null` (logged as warnings)
+- Email send failures trigger retry logic with exponential backoff
+- Final failures stored in FailedEmail collection for manual review
+
+### Error Categories and Responses
+
+**Transport Initialization Errors** (Credentials):
+- Incomplete credentials → Return `null`, log warning, set `isMailerSetup = false`
+- Type guard validation: `credential === undefined` (OAuth2 module)
+- Null check validation: `host == null` (SMTP/SES modules)
+
+**Email Send Errors** (Runtime):
+- OAuth2 token refresh failure (invalid_grant) → Retry with backoff, store failed email after max retries
+- SMTP/SES connection errors → Retry with backoff
+- Validation errors (missing recipient) → Immediate failure (no retry)
+
+**Type Errors** (Compile-time):
+- Empty string assigned to OAuth2 credential → TypeScript compile error
+- Invalid type passed to transport factory → TypeScript compile error
+
+### Monitoring
+
+No changes to existing monitoring approach:
+- Structured logging with tags: `oauth2_email_success`, `oauth2_token_refresh_failure`, `gmail_api_error`
+- Failed emails stored in MongoDB (FailedEmail collection)
+- S2S messaging for configuration change propagation
+
+## Testing Strategy
+
+### Unit Tests: Transport Modules
+
+1. **SMTP Transport**: Verify null return for missing host/port, successful transport creation with valid credentials
+2. **SES Transport**: Verify null return for missing AWS credentials, successful transport creation with access key/secret
+3. **OAuth2 Transport**: Verify null return for incomplete/blank credentials, successful transport creation with NonBlankString credentials, type guard behavior (`=== undefined`)
+4. **Types Module**: Verify StrictOAuth2Options compatibility with SMTPTransport.Options (type-level test)
+
+### Integration Tests: MailService
+
+1. **Initialization Flow**: Verify MailService delegates to correct transport module based on `mail:transmissionMethod` config
+2. **S2S Messaging**: Verify configuration updates propagate via S2sMessageHandlable interface
+3. **Backward Compatibility**: Verify existing import paths resolve correctly (`~/server/service/mail`)
+4. **Error Handling**: Verify `send()` method preserves retry logic, exponential backoff, and failed email storage
+5. **Public API**: Verify all existing methods/properties remain accessible and functional
+
+### E2E Tests (Minimal)
+
+1. **Email Sending**: Verify end-to-end email send with OAuth2 transport (using test credentials)
+2. **Config API Integration**: Verify mail settings can be updated via API and MailService re-initializes
+
+### Type Safety Tests
+
+1. **Compile-time Validation**: Verify empty string assignment to OAuth2 credential produces TypeScript error
+2. **Type Guard Behavior**: Verify `toNonBlankStringOrUndefined()` return type correctly narrows to `NonBlankString | undefined`
+3. **Compatibility**: Verify StrictOAuth2Options can be passed to `nodemailer.createTransport()` without type errors
+
+### Test Coverage Targets
+
+- **Transport modules**: 100% (simple factory functions, easy to test in isolation)
+- **MailService**: Maintain existing coverage (~80%+)
+- **Types**: Type-level tests only (no runtime testing needed)
+
+## Security Considerations
+
+### Credential Handling
+
+**Existing security measures maintained**:
+- Credentials retrieved from ConfigManager (never hardcoded)
+- `maskCredential()` helper for logging (shows only last 4 characters)
+- Failed email storage excludes sensitive credential details
+
+**Improvements**:
+- Type-safe credential validation prevents accidental empty string credentials
+- Compile-time checks reduce risk of runtime credential errors
+
+### TLS/SSL Configuration
+
+**Existing behavior maintained**:
+- SMTP transport uses `rejectUnauthorized: false` for self-signed certificates
+- OAuth2 transport uses Gmail's secure API endpoints
+- SES transport uses AWS SDK defaults (TLS enabled)
+
+**Recommendation**: Document requirement for valid TLS certificates in production (out of scope for this refactoring)
+
+## Migration Strategy
+
+### Phase 1: Preparation
+
+1. Install `@types/nodemailer@6.4.22` as devDependency
+2. Create `src/server/service/mail/` directory
+3. Verify all existing tests pass before refactoring
+
+### Phase 2: Module Extraction
+
+1. Create `types.ts` with shared type definitions (StrictOAuth2Options, MailConfig, EmailConfig, SendResult)
+2. Extract SMTP logic to `smtp.ts` with `createSMTPClient()` function
+3. Extract SES logic to `ses.ts` with `createSESClient()` function
+4. Extract OAuth2 logic to `oauth2.ts` with `createOAuth2Client()` function (using NonBlankString)
+5. Create co-located test files for each transport module
+
+### Phase 3: MailService Refactoring
+
+1. Move `src/server/service/mail.ts` to `src/server/service/mail/mail.ts`
+2. Update MailService to import and delegate to transport modules
+3. Remove inline `createSMTPClient()`, `createSESClient()`, `createOAuth2Client()` methods
+4. Move `src/server/service/mail.spec.ts` to `src/server/service/mail/mail.spec.ts`
+5. Update test file imports
+
+### Phase 4: Barrel Export
+
+1. Create `src/server/service/mail/index.ts` with default export
+2. Verify all existing imports resolve correctly (no changes needed in consuming code)
+3. Run full test suite to verify backward compatibility
+
+### Phase 5: Validation
+
+1. Run type checker (`pnpm run lint:typecheck`)
+2. Run linter (`pnpm run lint:biome`)
+3. Run full test suite (`pnpm run test`)
+4. Run integration tests with real SMTP/OAuth2 credentials
+5. Verify email sending works in development environment
+
+### Rollback Plan
+
+If issues arise after deployment:
+1. Revert commit to restore single-file MailService implementation
+2. Remove `@types/nodemailer` dependency if causing conflicts
+3. No database migrations or data changes required (pure code refactoring)
+
+## Supporting References
+
+### Package Dependencies
+
+```json
+{
+  "dependencies": {
+    "nodemailer": "6.9.15",
+    "nodemailer-ses-transport": "^1.5.1",
+    "ejs": "^3.1.9",
+    "@growi/core": "workspace:*"
+  },
+  "devDependencies": {
+    "@types/nodemailer": "6.4.22"  // NEW
+  }
+}
+```
+
+### Directory Structure (After Refactoring)
+
+```
+src/server/service/mail/
+├── index.ts              # Barrel export (default: MailService)
+├── mail.ts               # MailService class (orchestration)
+├── mail.spec.ts          # MailService tests
+├── smtp.ts               # SMTP transport factory
+├── smtp.spec.ts          # SMTP transport tests
+├── ses.ts                # SES transport factory
+├── ses.spec.ts           # SES transport tests
+├── oauth2.ts             # OAuth2 transport factory (with NonBlankString)
+├── oauth2.spec.ts        # OAuth2 transport tests
+└── types.ts              # Shared type definitions (StrictOAuth2Options, etc.)
+```
+
+### Config Key Dependencies
+
+**SMTP** (`smtp.ts`):
+- `mail:smtpHost` (required)
+- `mail:smtpPort` (required)
+- `mail:smtpUser` (optional, for auth)
+- `mail:smtpPassword` (optional, for auth)
+
+**SES** (`ses.ts`):
+- `mail:sesAccessKeyId` (required)
+- `mail:sesSecretAccessKey` (required)
+
+**OAuth2** (`oauth2.ts`):
+- `mail:oauth2User` (required, NonBlankString)
+- `mail:oauth2ClientId` (required, NonBlankString)
+- `mail:oauth2ClientSecret` (required, NonBlankString)
+- `mail:oauth2RefreshToken` (required, NonBlankString)
+
+**Common** (all transports):
+- `mail:from` (required, sender address)
+- `mail:transmissionMethod` (required, one of: 'smtp', 'ses', 'oauth2')

+ 142 - 0
.kiro/specs/refactor-mailer-service/requirements.md

@@ -0,0 +1,142 @@
+# Requirements Document
+
+## Project Description (Input)
+
+### Core Objectives
+
+1. **Directory Reorganization**
+   - Move `mail.ts` and `mail.spec.ts` into a `mail/` directory
+   - Create `mail/index.ts` as a barrel file, exporting only public API
+
+2. **Module Separation by Feature**
+   - Split MailService into separate modules: `smtp`, `ses`, `oauth2`
+   - Each transmission method should be in its own module for better maintainability
+
+3. **Type Safety Improvements**
+   - Replace runtime falsy checks (`!clientId || !clientSecret || !refreshToken || !user`) with TypeScript type guards
+   - Use @growi/core's `NonBlankString` type for OAuth2 credentials
+   - Install `@types/nodemailer` for proper type definitions
+   - Define stricter types than nodemailer provides (e.g., `StrictOAuth2Options`)
+   - Prevent empty strings at compile time rather than runtime
+
+### Guiding Principles
+- Incremental improvement: perfection is not required in this iteration
+- Maintain backward compatibility with existing MailService API
+- Follow GROWI's coding standards (immutability, named exports, co-located tests)
+
+## Requirements
+
+### Introduction
+
+This refactoring effort modernizes the MailService implementation to improve code organization, maintainability, and type safety. The refactoring focuses on three key areas: restructuring the file organization to follow GROWI's feature-based architecture, separating transmission methods (SMTP, SES, OAuth2) into distinct modules, and leveraging TypeScript's type system to prevent runtime errors related to empty credentials. The refactoring maintains full backward compatibility with the existing MailService API to ensure zero disruption to dependent code.
+
+---
+
+### Requirement 1: Directory Restructuring
+
+**Objective:** As a developer, I want the mail-related files organized in a dedicated `mail/` directory with a barrel export pattern, so that the codebase follows GROWI's feature-based structure and provides clear module boundaries.
+
+#### Acceptance Criteria
+
+1. The MailService implementation shall be located at `src/server/service/mail/mail.ts`
+2. The MailService test file shall be located at `src/server/service/mail/mail.spec.ts`
+3. The mail module shall provide a barrel file at `src/server/service/mail/index.ts` that exports the public API (MailService class)
+4. When external code imports MailService, it shall import from `~/server/service/mail` without referencing internal module structure
+5. The legacy import path `~/server/service/mail` shall continue to work after refactoring (via barrel export)
+
+---
+
+### Requirement 2: Module Separation by Transmission Method
+
+**Objective:** As a developer, I want each email transmission method (SMTP, SES, OAuth2) in separate modules, so that code is easier to maintain, test, and extend independently.
+
+#### Acceptance Criteria
+
+1. The mail module shall provide a separate module for SMTP transport at `src/server/service/mail/smtp.ts`
+2. The mail module shall provide a separate module for SES transport at `src/server/service/mail/ses.ts`
+3. The mail module shall provide a separate module for OAuth2 transport at `src/server/service/mail/oauth2.ts`
+4. When MailService initializes, it shall delegate transport creation to the appropriate module based on `mail:transmissionMethod` config value
+5. Each transport module shall export a function with signature `create[Transport]Client(configManager: IConfigManagerForApp): Transporter | null`
+6. When a transport module receives incomplete credentials, it shall return `null` and log a warning
+7. The mail module structure shall follow GROWI's immutability and named export conventions
+
+---
+
+### Requirement 3: Type-Safe OAuth2 Implementation
+
+**Objective:** As a developer, I want OAuth2 credentials validated at compile time using TypeScript's type system, so that empty string credentials are prevented before runtime and credential-related errors are caught during development.
+
+#### Acceptance Criteria
+
+**Type Infrastructure** (3.1-3.3):
+1. The project shall include `@types/nodemailer` as a development dependency for nodemailer type definitions
+2. The OAuth2 module shall define a `StrictOAuth2Options` type that requires `NonBlankString` for all credential fields (`user`, `clientId`, `clientSecret`, `refreshToken`)
+3. The `StrictOAuth2Options` type shall be compatible with nodemailer's `SMTPTransport.Options` interface
+
+**Runtime Validation** (3.4-3.6):
+4. When the OAuth2 module retrieves credentials from config, it shall use `toNonBlankStringOrUndefined()` helper from `@growi/core`
+5. If any OAuth2 credential is `undefined` after conversion, the OAuth2 module shall return `null` without creating a transport
+6. The OAuth2 module shall use type guards (`credential === undefined`) instead of falsy checks (`!credential`)
+7. When all OAuth2 credentials are valid NonBlankStrings, the module shall construct a `StrictOAuth2Options` object
+
+**Compile-Time Safety** (3.7-3.11):
+8. When an empty string is assigned to an OAuth2 credential field, TypeScript shall produce a compile-time type error
+9. The `StrictOAuth2Options` type shall not accept `string | undefined` for credential fields
+10. When `toNonBlankStringOrUndefined()` is used, the return type shall be `NonBlankString | undefined`
+11. If TypeScript strict mode is enabled, all transport modules shall compile without type errors
+12. The OAuth2 module shall not use `any` type for credentials or transport options
+
+---
+
+### Requirement 4: Backward Compatibility
+
+**Objective:** As a system integrator, I want the refactored MailService to maintain the same public API as the current implementation, so that dependent code continues to work without modification.
+
+#### Acceptance Criteria
+
+1. The MailService class shall maintain its current constructor signature: `constructor(crowi: Crowi)`
+2. The MailService shall continue to implement the `S2sMessageHandlable` interface without changes
+3. When external code calls `mailService.send(config)`, it shall work identically to the pre-refactoring implementation
+4. The MailService shall expose the same public properties: `isMailerSetup`, `mailer`, `mailConfig`
+5. When MailService is imported from `~/server/service/mail`, it shall resolve to the refactored implementation seamlessly
+6. The refactored implementation shall not change the behavior of retry logic, error handling, or failed email storage
+
+---
+
+### Requirement 5: Co-located Testing
+
+**Objective:** As a developer, I want test files co-located with their source modules, so that tests are easy to find and maintain alongside the code they verify.
+
+#### Acceptance Criteria
+
+1. The main MailService test shall be located at `src/server/service/mail/mail.spec.ts`
+2. The SMTP transport module shall have a co-located test at `src/server/service/mail/smtp.spec.ts`
+3. The SES transport module shall have a co-located test at `src/server/service/mail/ses.spec.ts`
+4. The OAuth2 transport module shall have a co-located test at `src/server/service/mail/oauth2.spec.ts`
+5. When tests are executed, all mail-related tests shall be discovered and run by the test runner
+6. Each transport module test shall verify `null` return behavior for incomplete credentials
+7. The OAuth2 module test shall verify that empty strings are rejected at the type level
+
+---
+
+### Non-Functional Requirements
+
+#### Code Quality
+
+1. All refactored code shall follow GROWI's coding standards defined in `.claude/rules/coding-style.md`
+2. All modules shall use named exports (no default exports except for Next.js pages)
+3. All functions shall use immutable patterns (no object mutation)
+4. All new code shall include TypeScript type annotations for parameters and return values
+
+#### Documentation
+
+1. Each transport module shall include JSDoc comments for exported functions
+2. The `StrictOAuth2Options` type shall include inline documentation explaining why it's stricter than nodemailer's default types
+
+#### Testing
+
+1. All transport modules shall maintain or improve current test coverage
+2. Tests shall verify both happy path and error scenarios (incomplete credentials, invalid configs)
+3. Type guard behavior shall be verified with unit tests
+
+

+ 145 - 0
.kiro/specs/refactor-mailer-service/research.md

@@ -0,0 +1,145 @@
+# Research & Design Decisions
+
+---
+**Purpose**: Capture discovery findings, architectural investigations, and rationale for MailService refactoring.
+---
+
+## Summary
+- **Feature**: `refactor-mailer-service`
+- **Discovery Scope**: Extension (Brownfield Refactoring)
+- **Key Findings**:
+  - Existing MailService is monolithic with ~408 lines containing all transport logic
+  - Current implementation uses runtime falsy checks for OAuth2 credentials
+  - @types/nodemailer@6.4.22 provides type definitions but allows `string | undefined` (too permissive)
+  - GROWI already has NonBlankString type infrastructure in @growi/core
+  - Existing test file (mail.spec.ts) uses Vitest with comprehensive mocking
+
+## Research Log
+
+### Current MailService Architecture
+
+- **Context**: Understanding existing implementation before refactoring
+- **Current Structure**:
+  - Single file: `src/server/service/mail.ts` (~408 lines)
+  - Three transport creation methods inline: `createSMTPClient()`, `createSESClient()`, `createOAuth2Client()`
+  - Public API: constructor, `send()`, `initialize()`, S2sMessageHandlable interface implementation
+  - Dependencies: nodemailer, ejs (templating), FailedEmail model
+- **Findings**:
+  - Transport creation logic is tightly coupled to MailService class
+  - No clear separation of concerns between transmission methods
+  - OAuth2 implementation uses falsy checks: `if (!clientId || !clientSecret || !refreshToken || !user)`
+- **Implications**: Refactoring to separate modules will improve testability and maintainability without breaking external contracts
+
+### Import Usage Analysis
+
+- **Context**: Identifying all code that depends on MailService
+- **Sources Consulted**: Codebase grep for import patterns
+- **Findings**:
+  - Direct imports found in:
+    - `src/server/routes/apiv3/app-settings/index.ts` (config API)
+    - `src/server/service/global-notification/index.ts` (notification system)
+  - Import pattern: `import MailService from '~/server/service/mail'` (default export)
+  - Test file: `src/server/service/mail.spec.ts` (Vitest-based, mocks FailedEmail)
+- **Implications**: Barrel export at `mail/index.ts` must maintain default export compatibility
+
+### TypeScript Type Safety for OAuth2
+
+- **Context**: Investigating how to prevent empty string credentials at compile time
+- **Sources Consulted**:
+  - @types/nodemailer repository: https://github.com/DefinitelyTyped/DefinitelyTyped
+  - XOAuth2.Options interface definition
+  - @growi/core NonBlankString implementation
+- **Findings**:
+  - `@types/nodemailer` v6.4.22 exists (compatible with nodemailer v6.9.15)
+  - XOAuth2.Options interface defines: `user?`, `clientId?`, `clientSecret?`, `refreshToken?` (all optional, type: `string | undefined`)
+  - nodemailer runtime implementation uses falsy checks: `!this.options.refreshToken`
+  - GROWI's `NonBlankString` is a branded type: `string & { readonly __brand: unique symbol }`
+  - Helper functions available: `toNonBlankStringOrUndefined()`, `isNonBlankString()`
+- **Implications**:
+  - Install @types/nodemailer for basic type safety
+  - Define `StrictOAuth2Options` with NonBlankString fields (stricter than library types)
+  - Use type guards (`=== undefined`) instead of falsy checks for clarity
+
+### Testing Infrastructure
+
+- **Context**: Understanding current test setup for migration planning
+- **Sources Consulted**: mail.spec.ts, Vitest documentation
+- **Findings**:
+  - Test framework: Vitest
+  - Mocking approach: vi.mock() for FailedEmail model
+  - Test structure: describe/it blocks with beforeEach setup
+  - Mock pattern: Full Crowi object with configManager, s2sMessagingService, appService
+  - Existing tests cover: exponentialBackoff, sendWithRetry, storeFailedEmail
+- **Implications**: Each new transport module should follow same testing pattern with co-located spec files
+
+## Architecture Pattern Evaluation
+
+| Option | Description | Strengths | Risks / Limitations | Notes |
+|--------|-------------|-----------|---------------------|-------|
+| Factory Functions | Create transport factory functions in separate modules | Simple, functional, easy to test in isolation | MailService still coordinates initialization | Aligns with GROWI's functional programming style |
+| Strategy Pattern | Transport classes implementing common interface | OOP-style extensibility | Heavier abstraction for simple use case | Overengineering for current needs |
+| Inline Refactor | Extract methods to separate files, keep MailService structure | Minimal disruption | Less clear module boundaries | Doesn't fully address separation concerns |
+
+**Selected**: Factory Functions - clearest separation of concerns without over-abstraction.
+
+## Design Decisions
+
+### Decision: Module Structure and Barrel Export Pattern
+
+- **Context**: Need to reorganize files while maintaining backward compatibility
+- **Alternatives Considered**:
+  1. Named exports only - breaks existing imports
+  2. Default export + named exports - provides gradual migration path
+  3. Separate index.ts per module - over-complicated for single-class exports
+- **Selected Approach**: Single `mail/index.ts` barrel file with default export
+- **Rationale**: Maintains existing import pattern (`import MailService from '~/server/service/mail'`) while organizing internal structure
+- **Trade-offs**: Internal modules use named exports (best practice), barrel file provides default export (compatibility)
+- **Follow-up**: Verify all existing imports resolve correctly after refactoring
+
+### Decision: Transport Factory Function Signature
+
+- **Context**: Need consistent interface for all transport creation modules
+- **Alternatives Considered**:
+  1. `createClient(config: Config)` - requires Config type definition
+  2. `create[Transport]Client(configManager: IConfigManagerForApp)` - passes full configManager
+  3. Class-based transporters - OOP approach
+- **Selected Approach**: `create[Transport]Client(configManager: IConfigManagerForApp): Transporter | null`
+- **Rationale**:
+  - ConfigManager already available in MailService context
+  - Null return clearly signals initialization failure
+  - Function naming follows GROWI conventions
+- **Trade-offs**: Each module reads config keys directly (coupling to config structure)
+- **Follow-up**: Document config key dependencies in each module's JSDoc
+
+### Decision: StrictOAuth2Options Type Definition
+
+- **Context**: Prevent empty string credentials at compile time
+- **Alternatives Considered**:
+  1. Use @types/nodemailer types as-is - too permissive (allows empty strings)
+  2. Fork XOAuth2.Options with stricter types - maintenance burden
+  3. Define custom type with NonBlankString - leverages existing GROWI infrastructure
+- **Selected Approach**: Define `StrictOAuth2Options` extending nodemailer's structure with NonBlankString fields
+- **Rationale**:
+  - NonBlankString infrastructure already exists in @growi/core
+  - Compile-time validation prevents runtime errors
+  - Type is compatible with nodemailer's SMTPTransport.Options (structural typing)
+- **Trade-offs**: Additional type definition maintenance, but minimal overhead
+- **Follow-up**: Add inline documentation explaining why stricter than library default
+
+## Risks & Mitigations
+
+- **Risk**: Existing imports break after directory restructuring
+  - **Mitigation**: Barrel export maintains `~/server/service/mail` import path; verify with integration tests
+- **Risk**: Transport modules tightly coupled to ConfigManager structure
+  - **Mitigation**: Document config key dependencies in JSDoc; consider config abstraction in future iteration
+- **Risk**: StrictOAuth2Options type diverges from nodemailer updates
+  - **Mitigation**: Pin @types/nodemailer version; test with nodemailer updates before upgrading
+- **Risk**: Test suite execution time increases with more test files
+  - **Mitigation**: Co-located tests allow focused execution; Vitest runs in parallel by default
+
+## References
+
+- [@types/nodemailer on npm](https://www.npmjs.com/package/@types/nodemailer) - TypeScript definitions for nodemailer
+- [XOAuth2 Interface Definition](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/nodemailer/lib/xoauth2/index.d.ts) - OAuth2 credential types
+- [GROWI NonBlankString Implementation](../../packages/core/src/interfaces/primitive/string.ts) - Branded type pattern
+- [Nodemailer OAuth2 Documentation](https://nodemailer.com/smtp/oauth2) - Gmail API integration guide

+ 22 - 0
.kiro/specs/refactor-mailer-service/spec.json

@@ -0,0 +1,22 @@
+{
+  "feature_name": "refactor-mailer-service",
+  "created_at": "2026-02-10T08:15:00Z",
+  "updated_at": "2026-02-10T09:15:00Z",
+  "language": "en",
+  "phase": "tasks-generated",
+  "approvals": {
+    "requirements": {
+      "generated": true,
+      "approved": true
+    },
+    "design": {
+      "generated": true,
+      "approved": true
+    },
+    "tasks": {
+      "generated": true,
+      "approved": false
+    }
+  },
+  "ready_for_implementation": false
+}

+ 185 - 0
.kiro/specs/refactor-mailer-service/tasks.md

@@ -0,0 +1,185 @@
+# Implementation Tasks
+
+## Overview
+
+This refactoring follows a three-phase approach: establishing type safety foundations, extracting transport modules, and integrating them into the refactored MailService. The tasks ensure type-safe implementations from the beginning while maintaining backward compatibility throughout.
+
+---
+
+## Phase 1: Type Safety Foundation
+
+- [ ] 1. Establish type infrastructure for mail module
+- [ ] 1.1 Install nodemailer type definitions
+  - Add @types/nodemailer@6.4.22 as devDependency in package.json
+  - Run `pnpm install` to install the package
+  - Verify package appears in devDependencies
+  - _Requirements: 3.1_
+
+- [ ] 1.2 Create shared type definitions module
+  - Create src/server/service/mail/ directory
+  - Define StrictOAuth2Options type with NonBlankString credential fields (user, clientId, clientSecret, refreshToken)
+  - Define MailConfig, EmailConfig, SendResult types
+  - Add JSDoc comments explaining why StrictOAuth2Options is stricter than nodemailer defaults
+  - Include type assertion verifying StrictOAuth2Options compatibility with SMTPTransport.Options
+  - _Requirements: 3.2, 3.3, 3.9_
+
+- [ ] 1.3 Verify type safety infrastructure
+  - Run `pnpm run lint:typecheck` to verify type compilation
+  - Confirm no new type errors introduced
+  - Run `pnpm run test` to verify existing tests still pass
+  - Verify existing mail.ts file remains unchanged and functional
+  - _Requirements: 3.11_
+
+---
+
+## Phase 2: Module Extraction
+
+- [ ] 2. Extract transport modules with type-safe implementations
+- [ ] 2.1 (P) Extract SMTP transport module
+  - Create smtp.ts with createSMTPClient factory function
+  - Accept configManager parameter and optional SMTPTransport.Options
+  - Read mail:smtpHost, mail:smtpPort, mail:smtpUser, mail:smtpPassword from config
+  - Return null if host or port missing, log warning for incomplete credentials
+  - Configure TLS settings (rejectUnauthorized: false for self-signed certificates)
+  - Create smtp.spec.ts with tests verifying null return for missing host/port and successful transport creation
+  - Use named exports following GROWI conventions
+  - _Requirements: 2.1, 2.5, 2.6, 2.7, 5.2, 5.6_
+
+- [ ] 2.2 (P) Extract SES transport module
+  - Create ses.ts with createSESClient factory function
+  - Accept configManager parameter and optional SES configuration object
+  - Read mail:sesAccessKeyId, mail:sesSecretAccessKey from config
+  - Return null if access key or secret key missing, log warning for incomplete credentials
+  - Use nodemailer-ses-transport adapter for AWS SES integration
+  - Create ses.spec.ts with tests verifying null return for missing AWS credentials and successful transport creation
+  - Use named exports following GROWI conventions
+  - _Requirements: 2.2, 2.5, 2.6, 2.7, 5.3, 5.6_
+
+- [ ] 2.3 (P) Extract OAuth2 transport module with type-safe credentials
+  - Create oauth2.ts with createOAuth2Client factory function
+  - Accept configManager parameter and optional SMTPTransport.Options
+  - Read mail:oauth2User, mail:oauth2ClientId, mail:oauth2ClientSecret, mail:oauth2RefreshToken from config
+  - Use toNonBlankStringOrUndefined() helper from @growi/core to convert config values
+  - Implement type guards checking credential === undefined (not falsy checks)
+  - Return null if any credential is undefined after conversion, log warning for incomplete credentials
+  - Construct StrictOAuth2Options object when all credentials are valid NonBlankStrings
+  - Configure service as 'gmail' with OAuth2 auth type
+  - Create oauth2.spec.ts with tests verifying:
+    - Null return for incomplete/blank credentials
+    - Successful transport creation with NonBlankString credentials
+    - Type guard behavior (=== undefined)
+    - Empty strings rejected at type level (type-level test)
+  - Use named exports, no any types for credentials or transport options
+  - _Requirements: 2.3, 2.5, 2.6, 2.7, 3.4, 3.5, 3.6, 3.7, 3.8, 3.10, 3.12, 5.4, 5.6, 5.7_
+
+---
+
+## Phase 3: Integration & Barrel Export
+
+- [ ] 3. Integrate transport modules and establish barrel export
+- [ ] 3.1 Refactor MailService to delegate transport creation
+  - Import createSMTPClient, createSESClient, createOAuth2Client from respective modules
+  - Update initialize() method to delegate transport creation based on mail:transmissionMethod config
+  - Remove inline createSMTPClient(), createSESClient(), createOAuth2Client() method implementations
+  - Preserve all existing public methods: send(), initialize(), publishUpdatedMessage()
+  - Maintain S2sMessageHandlable interface implementation unchanged
+  - Preserve retry logic, error handling (sendWithRetry, storeFailedEmail), and credential masking (maskCredential)
+  - Keep existing public properties: isMailerSetup, mailer, mailConfig, lastLoadedAt
+  - Verify MailService behavior identical to pre-refactoring implementation
+  - _Requirements: 2.4, 4.1, 4.2, 4.3, 4.4, 4.6_
+
+- [ ] 3.2 Reorganize files into mail/ directory structure
+  - Move src/server/service/mail.ts to src/server/service/mail/mail.ts
+  - Move src/server/service/mail.spec.ts to src/server/service/mail/mail.spec.ts
+  - Update import paths in mail.spec.ts to reference local modules (./mail, ./smtp, ./ses, ./oauth2, ./types)
+  - Update import paths in mail.ts to reference transport modules
+  - Verify all imports resolve correctly after file moves
+  - _Requirements: 1.1, 1.2, 5.1_
+
+- [ ] 3.3 Create barrel export for backward compatibility
+  - Create src/server/service/mail/index.ts
+  - Export MailService as default export (maintains existing import pattern)
+  - Export MailConfig, EmailConfig, SendResult, StrictOAuth2Options as named exports
+  - Add JSDoc comment explaining barrel export maintains backward compatibility
+  - Verify import path ~/server/service/mail resolves to barrel export
+  - _Requirements: 1.3, 1.4, 1.5_
+
+- [ ] 3.4 Verify backward compatibility and test coverage
+  - Run full test suite with `pnpm run test`
+  - Verify all existing MailService tests pass without modification
+  - Confirm Config API (src/server/routes/apiv3/app-settings/index.ts) imports MailService successfully
+  - Confirm Global Notification Service (src/server/service/global-notification/index.ts) imports MailService successfully
+  - Run `pnpm run lint:typecheck` to verify no type errors
+  - Run `pnpm run lint:biome` to verify code style compliance
+  - Test email sending in development environment (SMTP, SES, OAuth2 if credentials available)
+  - Verify isMailerSetup, mailer, mailConfig properties accessible from external code
+  - _Requirements: 4.4, 4.5, 5.5_
+
+---
+
+## Requirements Coverage
+
+| Requirement | Tasks |
+|-------------|-------|
+| 1.1 | 3.2 |
+| 1.2 | 3.2 |
+| 1.3 | 3.3 |
+| 1.4 | 3.3 |
+| 1.5 | 3.3 |
+| 2.1 | 2.1 |
+| 2.2 | 2.2 |
+| 2.3 | 2.3 |
+| 2.4 | 3.1 |
+| 2.5 | 2.1, 2.2, 2.3 |
+| 2.6 | 2.1, 2.2, 2.3 |
+| 2.7 | 2.1, 2.2, 2.3 |
+| 3.1 | 1.1 |
+| 3.2 | 1.2 |
+| 3.3 | 1.2 |
+| 3.4 | 2.3 |
+| 3.5 | 2.3 |
+| 3.6 | 2.3 |
+| 3.7 | 2.3 |
+| 3.8 | 2.3 |
+| 3.9 | 1.2 |
+| 3.10 | 2.3 |
+| 3.11 | 1.3 |
+| 3.12 | 2.3 |
+| 4.1 | 3.1 |
+| 4.2 | 3.1 |
+| 4.3 | 3.1 |
+| 4.4 | 3.1, 3.4 |
+| 4.5 | 3.4 |
+| 4.6 | 3.1 |
+| 5.1 | 3.2 |
+| 5.2 | 2.1 |
+| 5.3 | 2.2 |
+| 5.4 | 2.3 |
+| 5.5 | 3.4 |
+| 5.6 | 2.1, 2.2, 2.3 |
+| 5.7 | 2.3 |
+
+---
+
+## Execution Notes
+
+### Parallel Execution
+Tasks marked with **(P)** can be executed in parallel:
+- **Phase 2 (2.1, 2.2, 2.3)**: All three transport module extractions operate on separate files with no shared dependencies
+
+### Checkpoints
+After each phase, verify:
+- **Phase 1**: Type definitions compile, existing tests pass, no behavior changes
+- **Phase 2**: All transport modules have passing tests, can be imported independently
+- **Phase 3**: Full integration tests pass, backward compatibility confirmed
+
+### Testing Strategy
+- **Phase 1**: Run existing test suite (no new tests needed)
+- **Phase 2**: Create co-located tests for each transport module (smtp.spec.ts, ses.spec.ts, oauth2.spec.ts)
+- **Phase 3**: Verify existing MailService tests pass, test external integration points
+
+### Rollback Plan
+If issues arise:
+1. **After Phase 1**: Remove types.ts and @types/nodemailer dependency
+2. **After Phase 2**: Delete transport modules and their tests
+3. **After Phase 3**: Revert file moves using git, restore original mail.ts structure