design.md 30 KB

OAuth 2.0 Email Support - Technical Design

Overview

This feature adds OAuth 2.0 authentication support for sending emails through Google Workspace accounts in GROWI. Administrators can configure email transmission using OAuth 2.0 credentials (Client ID, Client Secret, Refresh Token) instead of traditional SMTP passwords. This integration extends the existing mail service architecture while maintaining full backward compatibility with SMTP and SES configurations.

Purpose: Enable secure, token-based email authentication for Google Workspace accounts, improving security by eliminating password-based SMTP authentication and following Google's recommended practices for application email integration.

Users: GROWI administrators configuring email transmission settings will use the new OAuth 2.0 option alongside existing SMTP and SES methods.

Impact: Extends the mail service to support a third transmission method (oauth2) without modifying existing SMTP or SES functionality. No breaking changes to existing deployments.

Goals

  • Add OAuth 2.0 as a transmission method option in mail settings
  • Support Google Workspace email sending via Gmail API with OAuth 2.0 credentials
  • Maintain backward compatibility with existing SMTP and SES configurations
  • Provide consistent admin UI experience following SMTP/SES patterns
  • Implement automatic OAuth 2.0 token refresh using nodemailer's built-in support
  • Ensure secure storage and handling of OAuth 2.0 credentials

Non-Goals

  • OAuth 2.0 providers beyond Google Workspace (Microsoft 365, generic OAuth 2.0 servers)
  • Migration tool from SMTP to OAuth 2.0 (administrators manually reconfigure)
  • Authorization flow UI for obtaining refresh tokens (documented external process via Google Cloud Console)
  • Multi-account or account rotation support (single OAuth 2.0 account per instance)
  • Email queuing or rate limiting specific to OAuth 2.0 (relies on existing mail service behavior)

Architecture

Existing Architecture Analysis

Current Mail Service Implementation:

  • Service Location: apps/app/src/server/service/mail.ts (MailService class)
  • Initialization: MailService instantiated from Crowi container, loaded on app startup
  • Transmission Methods: Currently supports 'smtp' and 'ses' via mail:transmissionMethod config
  • Factory Pattern: createSMTPClient() and createSESClient() create nodemailer transports
  • Configuration: ConfigManager loads settings from MongoDB via mail:* namespace keys
  • S2S Messaging: Supports distributed config updates via mailServiceUpdated events
  • Test Email: SMTP-only test email functionality in admin UI

Current Admin UI Structure:

  • Main Component: MailSetting.tsx - form container with transmission method radio buttons
  • Sub-Components: SmtpSetting.tsx, SesSetting.tsx - conditional rendering based on selected method
  • State Management: AdminAppContainer (unstated) manages form state and API calls
  • Form Library: react-hook-form for validation and submission
  • API Integration: updateMailSettingHandler() saves all mail settings via REST API

Integration Points:

  • Config definition in config-definition.ts (add OAuth 2.0 keys)
  • MailService initialize() method (add OAuth 2.0 branch)
  • MailSetting.tsx transmission method array (add 'oauth2' option)
  • AdminAppContainer state methods (add OAuth 2.0 credential methods)

Architecture Pattern & Boundary Map

graph TB
    subgraph "Client Layer"
        MailSettingUI[MailSetting Component]
        OAuth2SettingUI[OAuth2Setting Component]
        SmtpSettingUI[SmtpSetting Component]
        SesSettingUI[SesSetting Component]
        AdminContainer[AdminAppContainer]
    end

    subgraph "API Layer"
        AppSettingsAPI[App Settings API]
        MailTestAPI[Mail Test API]
    end

    subgraph "Service Layer"
        MailService[MailService]
        ConfigManager[ConfigManager]
        S2SMessaging[S2S Messaging]
    end

    subgraph "External Services"
        GoogleOAuth[Google OAuth 2.0 API]
        GmailAPI[Gmail API]
        SMTPServer[SMTP Server]
        SESAPI[AWS SES API]
    end

    subgraph "Data Layer"
        MongoDB[(MongoDB Config)]
    end

    MailSettingUI --> AdminContainer
    OAuth2SettingUI --> AdminContainer
    SmtpSettingUI --> AdminContainer
    SesSettingUI --> AdminContainer

    AdminContainer --> AppSettingsAPI
    AdminContainer --> MailTestAPI

    AppSettingsAPI --> ConfigManager
    MailTestAPI --> MailService

    MailService --> ConfigManager
    MailService --> S2SMessaging

    ConfigManager --> MongoDB

    MailService --> GoogleOAuth
    MailService --> GmailAPI
    MailService --> SMTPServer
    MailService --> SESAPI

    S2SMessaging -.->|mailServiceUpdated| MailService

Architecture Integration:

  • Selected Pattern: Factory Method Extension - adds createOAuth2Client() to existing MailService factory methods
  • Domain Boundaries:
    • Client: Admin UI components for OAuth 2.0 configuration (follows existing SmtpSetting/SesSetting pattern)
    • Service: MailService handles all transmission methods; OAuth 2.0 isolated in new factory method
    • Config: ConfigManager persists OAuth 2.0 credentials using mail:oauth2* namespace
    • External: Google OAuth 2.0 API for token management; Gmail API for email transmission
  • Existing Patterns Preserved:
    • Transmission method selection pattern (radio buttons, conditional rendering)
    • Factory method pattern for transport creation
    • Config namespace pattern (mail:* keys)
    • Unstated container state management
    • S2S messaging for distributed config updates
  • New Components Rationale:
    • OAuth2Setting Component: Maintains UI consistency with SMTP/SES; enables modular development
    • createOAuth2Client() Method: Isolates OAuth 2.0 transport logic; follows existing factory pattern
    • Four Config Keys: Minimal set for OAuth 2.0 (user, clientId, clientSecret, refreshToken)
  • Steering Compliance:
    • Feature-based organization (mail service domain)
    • Named exports throughout
    • Type safety with explicit TypeScript interfaces
    • Immutable config updates
    • Security-first credential handling

Technology Stack

Layer Choice / Version Role in Feature Notes
Frontend React 18.x + TypeScript OAuth2Setting UI component Existing stack, no new dependencies
Frontend react-hook-form Form validation and state Existing dependency, consistent with SmtpSetting/SesSetting
Backend Node.js + TypeScript MailService OAuth 2.0 integration Existing runtime, no version changes
Backend nodemailer 6.x OAuth 2.0 transport creation Existing dependency with built-in OAuth 2.0 support
Data MongoDB Config storage for OAuth 2.0 credentials Existing database, new config keys only
External Google OAuth 2.0 API Token refresh endpoint Standard Google API, https://oauth2.googleapis.com/token
External Gmail API Email transmission via OAuth 2.0 Accessed via nodemailer Gmail transport

Key Technology Decisions:

  • Nodemailer OAuth 2.0: Built-in support eliminates need for additional OAuth 2.0 libraries; automatic token refresh reduces complexity
  • No New Dependencies: Feature fully implemented with existing packages; zero dependency risk
  • MongoDB Encryption: Credentials stored using existing ConfigManager encryption (same as SMTP passwords)
  • Gmail Service Shortcut: Nodemailer's service: "gmail" simplifies configuration and handles Gmail API specifics

System Flows

OAuth 2.0 Configuration Flow

sequenceDiagram
    participant Admin as Administrator
    participant UI as MailSetting UI
    participant Container as AdminAppContainer
    participant API as App Settings API
    participant Config as ConfigManager
    participant DB as MongoDB

    Admin->>UI: Select "oauth2" transmission method
    UI->>UI: Render OAuth2Setting component
    Admin->>UI: Enter OAuth 2.0 credentials
    Admin->>UI: Click Update button
    UI->>Container: handleSubmit formData
    Container->>API: POST app-settings
    API->>API: Validate OAuth 2.0 fields
    alt Validation fails
        API-->>Container: 400 Bad Request
        Container-->>UI: Display error toast
    else Validation passes
        API->>Config: setConfig mail:oauth2*
        Config->>DB: Save encrypted credentials
        DB-->>Config: Success
        Config-->>API: Success
        API-->>Container: 200 OK
        Container-->>UI: Display success toast
    end

Email Sending with OAuth 2.0 Flow

sequenceDiagram
    participant App as GROWI Application
    participant Mail as MailService
    participant Nodemailer as Nodemailer Transport
    participant Google as Google OAuth 2.0 API
    participant Gmail as Gmail API

    App->>Mail: send emailConfig
    Mail->>Mail: Check mailer setup
    alt Mailer not setup
        Mail-->>App: Error Mailer not set up
    else Mailer setup oauth2
        Mail->>Nodemailer: sendMail mailConfig
        Nodemailer->>Nodemailer: Check access token validity
        alt Access token expired
            Nodemailer->>Google: POST token refresh
            Google-->>Nodemailer: New access token
            Nodemailer->>Nodemailer: Cache access token
        end
        Nodemailer->>Gmail: POST send message
        alt Authentication failure
            Gmail-->>Nodemailer: 401 Unauthorized
            Nodemailer-->>Mail: Error Invalid credentials
            Mail-->>App: Error with OAuth 2.0 details
        else Success
            Gmail-->>Nodemailer: 200 OK message ID
            Nodemailer-->>Mail: Success
            Mail->>Mail: Log transmission success
            Mail-->>App: Email sent successfully
        end
    end

Flow-Level Decisions:

  • Token Refresh: Handled entirely by nodemailer; MailService does not implement custom refresh logic
  • Error Handling: OAuth 2.0 errors logged with specific Google API error codes for admin troubleshooting
  • Credential Validation: Performed at API layer before persisting to database; prevents invalid config states
  • S2S Sync: OAuth 2.0 config changes trigger mailServiceUpdated event for distributed deployments (existing pattern)

Requirements Traceability

Requirement Summary Components Interfaces Flows
1.1 Add OAuth 2.0 transmission method option MailSetting.tsx, config-definition.ts ConfigDefinition Configuration
1.2 Display OAuth 2.0 config fields when selected OAuth2Setting.tsx, MailSetting.tsx React Props Configuration
1.3 Validate email address format AdminAppContainer, App Settings API API Contract Configuration
1.4 Validate non-empty OAuth 2.0 credentials AdminAppContainer, App Settings API API Contract Configuration
1.5 Securely store OAuth 2.0 credentials with encryption ConfigManager, MongoDB Data Model Configuration
1.6 Confirm successful configuration save AdminAppContainer, MailSetting.tsx API Contract Configuration
1.7 Display descriptive error messages on save failure AdminAppContainer, MailSetting.tsx API Contract Configuration
2.1 Use nodemailer Gmail OAuth 2.0 transport MailService.createOAuth2Client() Service Interface Email Sending
2.2 Authenticate to Gmail API with OAuth 2.0 MailService.createOAuth2Client() External API Email Sending
2.3 Set FROM address to configured email MailService.setupMailConfig() Service Interface Email Sending
2.4 Log successful email transmission MailService.send() Service Interface Email Sending
2.5 Support all email content types MailService.send() (existing) Service Interface Email Sending
2.6 Process email queue sequentially MailService.send() (existing) Service Interface Email Sending
3.1 Use nodemailer automatic token refresh Nodemailer OAuth 2.0 transport External Library Email Sending
3.2 Request new access token with refresh token Nodemailer OAuth 2.0 transport External API Email Sending
3.3 Continue email sending after token refresh Nodemailer OAuth 2.0 transport External Library Email Sending
3.4 Log error and notify admin on refresh failure MailService.send(), Error Handler Service Interface Email Sending
3.5 Cache access tokens in memory Nodemailer OAuth 2.0 transport External Library Email Sending
3.6 Invalidate cached tokens on config update MailService.initialize() Service Interface Configuration
4.1 Display OAuth 2.0 form with consistent styling OAuth2Setting.tsx React Component Configuration
4.2 Preserve OAuth 2.0 credentials when switching methods AdminAppContainer state State Management Configuration
4.3 Provide field-level help text OAuth2Setting.tsx React Component Configuration
4.4 Mask sensitive fields (last 4 characters) OAuth2Setting.tsx React Component Configuration
4.5 Provide test email button MailSetting.tsx API Contract Email Sending
4.6 Display test email result with detailed errors AdminAppContainer, MailSetting.tsx API Contract Email Sending
5.1 Log specific OAuth 2.0 error codes MailService error handler Service Interface Email Sending
5.2 Retry email sending with exponential backoff MailService.send() Service Interface Email Sending
5.3 Store failed emails after all retries MailService.send() Service Interface Email Sending
5.4 Never log credentials in plain text MailService, ConfigManager Security Pattern All flows
5.5 Require admin authentication for config page App Settings API API Contract Configuration
5.6 Stop OAuth 2.0 sending when credentials deleted MailService.initialize() Service Interface Email Sending
5.7 Validate SSL/TLS for OAuth 2.0 endpoints Nodemailer OAuth 2.0 transport External Library Email Sending
6.1 Maintain backward compatibility with SMTP/SES MailService, config-definition.ts All Interfaces All flows
6.2 Use only active transmission method MailService.initialize() Service Interface Email Sending
6.3 Allow switching transmission methods without data loss AdminAppContainer, ConfigManager State Management Configuration
6.4 Display configuration error if no method set MailService, MailSetting.tsx Service Interface Configuration
6.5 Expose OAuth 2.0 status via admin API App Settings API API Contract Configuration

Components and Interfaces

Component Summary

Component Domain/Layer Intent Req Coverage Key Dependencies (P0/P1) Contracts
MailService Server/Service Email transmission with OAuth 2.0 support 2.1-2.6, 3.1-3.6, 5.1-5.7, 6.2, 6.4 ConfigManager (P0), Nodemailer (P0), S2SMessaging (P1) Service
OAuth2Setting Client/UI OAuth 2.0 credential input form 1.2, 4.1, 4.3, 4.4 AdminAppContainer (P0), react-hook-form (P0) State
AdminAppContainer Client/State State management for mail settings 1.3, 1.4, 1.6, 1.7, 4.2, 6.3 App Settings API (P0) API
ConfigManager Server/Service Persist OAuth 2.0 credentials 1.5, 6.1, 6.3 MongoDB (P0) Service, State
App Settings API Server/API Mail settings CRUD operations 1.3-1.7, 4.5-4.6, 5.5, 6.5 ConfigManager (P0), MailService (P1) API
Config Definition Server/Config OAuth 2.0 config schema 1.1, 6.1 None State

Server / Service Layer

MailService

Field Detail
Intent Extend email transmission service with OAuth 2.0 support using Gmail API
Requirements 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 5.1, 5.2, 5.3, 5.4, 5.6, 5.7, 6.2, 6.4
Owner / Reviewers Backend team

Responsibilities & Constraints

  • Create OAuth 2.0 nodemailer transport using Gmail service with credentials from ConfigManager
  • Handle OAuth 2.0 authentication failures and token refresh errors with specific error logging
  • Implement retry logic with exponential backoff (1s, 2s, 4s) for transient failures
  • Store failed emails after all retry attempts for manual review
  • Maintain single active transmission method (smtp, ses, or oauth2) per instance
  • Invalidate cached OAuth 2.0 tokens when configuration changes via S2S messaging

Dependencies

  • Inbound: Crowi container — service initialization (P0)
  • Inbound: Application modules — email sending requests (P0)
  • Inbound: S2S Messaging — config update notifications (P1)
  • Outbound: ConfigManager — load OAuth 2.0 credentials (P0)
  • Outbound: Nodemailer — create transport and send emails (P0)
  • External: Google OAuth 2.0 API — token refresh (P0)
  • External: Gmail API — email transmission (P0)

Contracts: Service [x]

Service Interface
interface MailServiceOAuth2Extension {
  /**
   * Create OAuth 2.0 nodemailer transport for Gmail
   */
  createOAuth2Client(option?: OAuth2TransportOptions): Transporter | null;

  /**
   * Send email with retry logic and error handling
   */
  sendWithRetry(config: EmailConfig, maxRetries?: number): Promise<SendResult>;

  /**
   * Store failed email for manual review
   */
  storeFailedEmail(config: EmailConfig, error: Error): Promise<void>;

  /**
   * Wait with exponential backoff
   */
  exponentialBackoff(attempt: number): Promise<void>;
}

interface OAuth2TransportOptions {
  user: string;
  clientId: string;
  clientSecret: string;
  refreshToken: string;
}

interface MailService {
  send(config: EmailConfig): Promise<void>;
  initialize(): void;
  isMailerSetup: boolean;
}

interface EmailConfig {
  to: string;
  from?: string;
  subject?: string;
  template: string;
  vars?: Record<string, unknown>;
}

interface SendResult {
  messageId: string;
  response: string;
  envelope: {
    from: string;
    to: string[];
  };
}
  • Preconditions:

    • ConfigManager loaded with valid mail:oauth2* configuration values
    • Nodemailer package version supports OAuth 2.0 (v6.x+)
    • Google OAuth 2.0 refresh token has https://mail.google.com/ scope
  • Postconditions:

    • OAuth 2.0 transport created with automatic token refresh enabled
    • isMailerSetup flag set to true when transport successfully created
    • Failed transport creation returns null and logs error
    • Successful email sends logged with messageId and recipient
    • Failed emails stored after retry exhaustion
  • Invariants:

    • Only one transmission method active at a time
    • Credentials never logged in plain text
    • Token refresh handled transparently by nodemailer
    • Retry backoff: 1s, 2s, 4s

ConfigManager

Field Detail
Intent Persist and retrieve OAuth 2.0 credentials with encryption
Requirements 1.5, 6.1, 6.3

Responsibilities & Constraints

  • Store four new OAuth 2.0 config keys with encryption
  • Support transmission method value 'oauth2'
  • Maintain all SMTP and SES config values when OAuth 2.0 is configured

Dependencies

  • Inbound: MailService, App Settings API (P0)
  • Outbound: MongoDB, Encryption Service (P0)

Contracts: Service [x] / State [x]

Service Interface
interface ConfigManagerOAuth2Extension {
  getConfig(key: 'mail:oauth2User'): string | undefined;
  getConfig(key: 'mail:oauth2ClientId'): string | undefined;
  getConfig(key: 'mail:oauth2ClientSecret'): string | undefined;
  getConfig(key: 'mail:oauth2RefreshToken'): string | undefined;
  getConfig(key: 'mail:transmissionMethod'): 'smtp' | 'ses' | 'oauth2' | undefined;

  setConfig(key: 'mail:oauth2User', value: string): Promise<void>;
  setConfig(key: 'mail:oauth2ClientId', value: string): Promise<void>;
  setConfig(key: 'mail:oauth2ClientSecret', value: string): Promise<void>;
  setConfig(key: 'mail:oauth2RefreshToken', value: string): Promise<void>;
  setConfig(key: 'mail:transmissionMethod', value: 'smtp' | 'ses' | 'oauth2'): Promise<void>;
}
State Management
  • State Model: OAuth 2.0 credentials stored as separate config documents in MongoDB
  • Persistence: Encrypted at write time; decrypted at read time
  • Consistency: Atomic writes per config key
  • Concurrency: Last-write-wins; S2S messaging for eventual consistency

Client / UI Layer

OAuth2Setting Component

Field Detail
Intent Render OAuth 2.0 credential input form with help text and field masking
Requirements 1.2, 4.1, 4.3, 4.4

Responsibilities & Constraints

  • Display four input fields with help text
  • Mask saved Client Secret and Refresh Token (show last 4 characters)
  • Follow SMTP/SES visual patterns
  • Use react-hook-form register

Dependencies

  • Inbound: MailSetting component (P0)
  • Outbound: AdminAppContainer (P1)
  • External: react-hook-form (P0)

Contracts: State [x]

State Management
interface OAuth2SettingProps {
  register: UseFormRegister<MailSettingsFormData>;
  adminAppContainer?: AdminAppContainer;
}

interface MailSettingsFormData {
  fromAddress: string;
  transmissionMethod: 'smtp' | 'ses' | 'oauth2';
  smtpHost: string;
  smtpPort: string;
  smtpUser: string;
  smtpPassword: string;
  sesAccessKeyId: string;
  sesSecretAccessKey: string;
  oauth2User: string;
  oauth2ClientId: string;
  oauth2ClientSecret: string;
  oauth2RefreshToken: string;
}

AdminAppContainer (Extension)

Field Detail
Intent Manage OAuth 2.0 credential state and API interactions
Requirements 1.3, 1.4, 1.6, 1.7, 4.2, 6.3

Responsibilities & Constraints

  • Add four state properties and setter methods
  • Include OAuth 2.0 credentials in API payload
  • Validate email format before API call
  • Display success/error toasts

Dependencies

  • Inbound: MailSetting, OAuth2Setting (P0)
  • Outbound: App Settings API (P0)

Contracts: State [x] / API [x]

State Management
interface AdminAppContainerOAuth2State {
  fromAddress?: string;
  transmissionMethod?: 'smtp' | 'ses' | 'oauth2';
  smtpHost?: string;
  smtpPort?: string;
  smtpUser?: string;
  smtpPassword?: string;
  sesAccessKeyId?: string;
  sesSecretAccessKey?: string;
  isMailerSetup: boolean;
  oauth2User?: string;
  oauth2ClientId?: string;
  oauth2ClientSecret?: string;
  oauth2RefreshToken?: string;
}

interface AdminAppContainerOAuth2Methods {
  changeOAuth2User(oauth2User: string): Promise<void>;
  changeOAuth2ClientId(oauth2ClientId: string): Promise<void>;
  changeOAuth2ClientSecret(oauth2ClientSecret: string): Promise<void>;
  changeOAuth2RefreshToken(oauth2RefreshToken: string): Promise<void>;
  updateMailSettingHandler(): Promise<void>;
}

Server / API Layer

App Settings API (Extension)

Field Detail
Intent Handle OAuth 2.0 credential CRUD operations with validation
Requirements 1.3, 1.4, 1.5, 1.6, 1.7, 4.5, 4.6, 5.5, 6.5

Responsibilities & Constraints

  • Accept OAuth 2.0 credentials in PUT request
  • Validate email format and non-empty credentials
  • Persist via ConfigManager
  • Trigger S2S messaging
  • Require admin authentication

Dependencies

  • Inbound: AdminAppContainer (P0)
  • Outbound: ConfigManager, MailService, S2S Messaging (P0/P1)

Contracts: API [x]

API Contract
Method Endpoint Request Response Errors
PUT /api/v3/app-settings UpdateMailSettingsRequest AppSettingsResponse 400, 401, 500
GET /api/v3/app-settings - AppSettingsResponse 401, 500
POST /api/v3/mail/send-test - TestEmailResponse 400, 401, 500

Request/Response Schemas:

interface UpdateMailSettingsRequest {
  'mail:from'?: string;
  'mail:transmissionMethod'?: 'smtp' | 'ses' | 'oauth2';
  'mail:smtpHost'?: string;
  'mail:smtpPort'?: string;
  'mail:smtpUser'?: string;
  'mail:smtpPassword'?: string;
  'mail:sesAccessKeyId'?: string;
  'mail:sesSecretAccessKey'?: string;
  'mail:oauth2User'?: string;
  'mail:oauth2ClientId'?: string;
  'mail:oauth2ClientSecret'?: string;
  'mail:oauth2RefreshToken'?: string;
}

interface AppSettingsResponse {
  appSettings: {
    'mail:from'?: string;
    'mail:transmissionMethod'?: 'smtp' | 'ses' | 'oauth2';
    'mail:smtpHost'?: string;
    'mail:smtpPort'?: string;
    'mail:smtpUser'?: string;
    'mail:sesAccessKeyId'?: string;
    'mail:oauth2User'?: string;
    'mail:oauth2ClientId'?: string;
  };
  isMailerSetup: boolean;
}

interface TestEmailResponse {
  success: boolean;
  message?: string;
  error?: {
    code: string;
    message: string;
  };
}

Server / Config Layer

Config Definition (Extension)

Field Detail
Intent Define OAuth 2.0 configuration schema with type safety
Requirements 1.1, 6.1

Config Schema:

const CONFIG_KEYS = [
  'mail:oauth2User',
  'mail:oauth2ClientId',
  'mail:oauth2ClientSecret',
  'mail:oauth2RefreshToken',
];

'mail:transmissionMethod': defineConfig<'smtp' | 'ses' | 'oauth2' | undefined>({
  defaultValue: undefined,
}),

'mail:oauth2User': defineConfig<string | undefined>({
  defaultValue: undefined,
}),
'mail:oauth2ClientId': defineConfig<string | undefined>({
  defaultValue: undefined,
}),
'mail:oauth2ClientSecret': defineConfig<string | undefined>({
  defaultValue: undefined,
  isSecret: true,
}),
'mail:oauth2RefreshToken': defineConfig<string | undefined>({
  defaultValue: undefined,
  isSecret: true,
}),

Data Models

Domain Model

Mail Configuration Aggregate:

  • Root Entity: MailConfiguration
  • Value Objects: TransmissionMethod, OAuth2Credentials, SmtpCredentials, SesCredentials
  • Business Rules: Only one transmission method active; OAuth2Credentials complete when all fields present
  • Invariants: Credentials encrypted; FROM address required

Logical Data Model

Structure Definition:

  • Entity: Config (MongoDB document)
  • Attributes: ns, key, value, createdAt, updatedAt
  • Natural Keys: ns field (unique)

Consistency & Integrity:

  • Transaction Boundaries: Each config key saved independently
  • Temporal Aspects: updatedAt tracked per entry

Physical Data Model

  • Config documents stored in MongoDB with ns/key/value pattern
  • FailedEmail documents track failed email attempts with error context
  • Encryption: AES-256 for clientSecret and refreshToken via environment-provided key

Data Contracts & Integration

API Data Transfer:

  • OAuth 2.0 credentials via JSON in PUT /api/v3/app-settings
  • Client Secret and Refresh Token never returned in GET responses

Cross-Service Data Management:

  • S2S messaging broadcasts mailServiceUpdated event
  • Eventual consistency across instances

Critical Implementation Constraints

Nodemailer XOAuth2 Compatibility (CRITICAL)

Constraint: OAuth 2.0 credential validation must use falsy checks (!value) not null checks (value != null) to match nodemailer's internal XOAuth2 handler behavior.

Rationale: Nodemailer's XOAuth2.generateToken() method uses !this.options.refreshToken at line 184, which rejects empty strings as invalid. Using != null checks in GROWI would allow empty strings through validation, causing runtime failures when nodemailer rejects them.

Implementation Pattern:

// ✅ CORRECT: Falsy check matches nodemailer behavior
if (!clientId || !clientSecret || !refreshToken || !user) {
  return null;
}

Impact: Affects MailService.createOAuth2Client(), ConfigManager validation, and API validators. All OAuth 2.0 credential checks must follow this pattern.

Reference: mail.ts:219-226, research.md


Credential Preservation Pattern (CRITICAL)

Constraint: PUT requests updating OAuth 2.0 configuration must only include secret fields (clientSecret, refreshToken) when non-empty values are provided, preventing accidental credential overwrites.

Rationale: Standard PUT pattern sending all form fields would overwrite secrets with empty strings when administrators update non-secret fields (from address, user email). GET endpoint returns undefined for secrets (not masked placeholders) to prevent re-submission of placeholder text.

Implementation Pattern:

// Build params with non-secret fields
const params = {
  'mail:oauth2ClientId': req.body.oauth2ClientId,
  'mail:oauth2User': req.body.oauth2User,
};

// Only include secrets if non-empty
if (req.body.oauth2ClientSecret) {
  params['mail:oauth2ClientSecret'] = req.body.oauth2ClientSecret;
}

Impact: Affects App Settings API PUT handler and any future API that updates OAuth 2.0 credentials.

Reference: apiv3/app-settings/index.ts:293-306, research.md


Gmail API FROM Address Behavior (LIMITATION)

Limitation: Gmail API rewrites FROM addresses to the authenticated account email unless send-as aliases are configured in Google Workspace.

Example:

Configured: mail:from = "notifications@example.com"
Authenticated: oauth2User = "admin@company.com"
Actual sent FROM: "admin@company.com"

Workaround: Google Workspace administrators must configure send-as aliases in Gmail Settings → Accounts and Import → Send mail as, then verify domain ownership.

Why This Happens: Gmail API security policy prevents email spoofing by restricting FROM addresses to authenticated accounts or verified aliases.

Impact: GROWI's mail:from configuration has limited effect with OAuth 2.0. Custom FROM addresses require Google Workspace configuration. This is expected Gmail behavior, not a GROWI limitation.

Reference: research.md


OAuth 2.0 Retry Integration (DESIGN DECISION)

Decision: OAuth 2.0 transmission uses sendWithRetry() with exponential backoff (1s, 2s, 4s), while SMTP/SES use direct sendMail() without retries.

Rationale: OAuth 2.0 token refresh can fail transiently due to network issues or Google API rate limiting. Exponential backoff provides resilience without overwhelming the API.

Implementation:

if (transmissionMethod === 'oauth2') {
  return this.sendWithRetry(mailConfig);
}
return this.mailer.sendMail(mailConfig);

Impact: OAuth 2.0 email failures are automatically retried, improving reliability for production deployments.

Reference: mail.ts:392-400