Skip to content

Add 'post send' command (blocked by SwiftMail #132)#19

Merged
odrobnik merged 7 commits into
mainfrom
feature/issue-9-send-command
Mar 24, 2026
Merged

Add 'post send' command (blocked by SwiftMail #132)#19
odrobnik merged 7 commits into
mainfrom
feature/issue-9-send-command

Conversation

@odrobnik
Copy link
Copy Markdown
Contributor

Summary

Implements #9: Command structure and documentation for sending draft emails via SMTP.

Status

🚧 BLOCKED by SwiftMail #132 (SMTP support)

This PR establishes the CLI interface and workflow documentation. The actual send implementation will be added once SwiftMail #132 lands.

Changes

  • βœ… Added post send <uid> command with comprehensive help text
  • βœ… Created PostError enum for structured error handling
  • βœ… Command throws .notImplemented with clear dependency message
  • βœ… Full workflow documentation in command help

Usage

# Current behavior (placeholder):
post send 1234 --server drobnik
Error: Not implemented: The 'post send' command requires SMTP support in SwiftMail.

Dependency: https://github.com/Cocoanetics/SwiftMail/issues/132

Future Workflow (once unblocked)

  1. Fetch draft from Drafts mailbox
  2. Send via SMTP with updated Date and Message-Id headers
  3. APPEND to Sent with \\Seen flag
  4. Permanently EXPUNGE draft (not trash)

Configuration

{
  "servers": {
    "example": {
      "smtp": {
        "host": "mail.example.com",
        "port": 587,
        "useTLS": false
      },
      "allowSending": true
    }
  }
}

Safety

  • Requires allowSending: true in server config
  • Optional --yes flag to skip confirmation prompt
  • Drafts are permanently deleted (EXPUNGE, not trash)

Testing

  • βœ… swift build -c release
  • βœ… swift test (8/8 passing)
  • βœ… Help text verified
  • βœ… Placeholder error message verified

Next Steps

  1. Wait for SwiftMail #132 to merge
  2. Implement actual SMTP send logic
  3. Add integration tests

Related

Closes #9

Implements #9: CLI structure and help documentation for sending
draft emails via SMTP.

Changes:
- Added 'post send <uid>' command with full usage docs
- Created PostError enum for structured error handling
- Command throws .notImplemented with dependency info

The command is fully documented but non-functional until
SwiftMail #132 (SMTP support) is implemented.

Workflow (once unblocked):
1. Fetch draft from Drafts mailbox
2. Send via SMTP (preserve threading headers)
3. APPEND to Sent with \Seen flag
4. Permanently EXPUNGE draft

Safety:
- Requires allowSending: true in server config
- Optional --yes flag to skip confirmation

Related:
- Blocked by: Cocoanetics/SwiftMail#132
- Implements: #9
Changes:
- Removed --draft-mailbox and --sent-mailbox flags
- Updated help text to document auto-detection workflow
- Drafts: \Drafts flag β†’ name matching fallback
- Sent: \Sent flag β†’ name matching fallback

Simpler and safer β€” no manual mailbox configuration needed.
SwiftMail 1.4.0 was released today with full SMTP support, unblocking
this feature.

Changes:
- Updated Package.swift to SwiftMail 1.4.0
- Added SMTPConfiguration to server config
- Implemented sendDraft() using SwiftMail's orchestrator:
  * Fetches draft from Drafts (auto-detected)
  * Sends via SMTP (IMAP credentials reused)
  * Appends to Sent (auto-detected)
  * Expunges draft from Drafts
- Added resolveCredentials() helper to IMAPConnectionManager
- Added smtpNotConfigured error case
- Implemented Send command with JSON output support

Usage:
  post send <uid> --server <id> [--yes]

Workflow (fully automatic):
1. Auto-detect Drafts folder (\\Drafts flag or name matching)
2. Fetch draft message
3. Connect to SMTP and authenticate
4. Send via SMTPServer
5. Auto-detect Sent folder (\\Sent flag or name matching)
6. Append sent message with \\Seen flag
7. Permanently EXPUNGE draft

Configuration:
  {
    "servers": {
      "example": {
        "smtp": {
          "host": "mail.example.com",
          "port": 587,
          "useTLS": false
        }
      }
    }
  }

Testing:
- βœ… swift build -c release
- βœ… swift test (8/8 passing)

Implements: #9
Dependency: SwiftMail#132 (now released as 1.4.0)
Changes:
- Added CredentialType enum (.imap, .smtp) to KeychainCredentialStore
- Updated all keychain methods to accept type parameter (defaults to .imap)
- SMTP credentials use kSecAttrProtocolSMTP protocol attribute
- Added resolveSMTPCredentials() to IMAPConnectionManager
  * Checks for SMTP keychain entry first
  * Falls back to IMAP credentials (most common case)
- Updated sendDraft() to use resolveSMTPCredentials()

Workflow:
1. Try SMTP keychain entry (kSecAttrDescription = "Post SMTP")
2. Fallback to IMAP credentials if no SMTP entry exists
3. Users can store separate SMTP credentials with:
   post credential add --server <id> --smtp

Testing:
- βœ… swift build -c release
- βœ… swift test (8/8 passing)
- βœ… Backward compatible (IMAP credentials still work)

This allows separate SMTP credentials when needed while maintaining
the simple case (same credentials for both).
SMTP credentials must now be explicitly configured. No automatic fallback
to IMAP credentials.

Changes:
- Updated resolveSMTPCredentials() to throw noSMTPCredentials error if not found
- Added PostConfigurationError.noSMTPCredentials case
- Error message guides users to: post credential set --server <id> --smtp

Rationale:
- Explicit configuration prevents accidental SMTP use of IMAP credentials
- Users must consciously set up SMTP credentials separately
- Clearer security boundary between IMAP and SMTP authentication

Testing:
- βœ… swift build -c release
- βœ… swift test (8/8 passing)
Implements fine-grained API key permissions with backward-compatible defaults.

Changes:
- Added optional scopes field to APIKeyRecord (nil = [imap] for backward compat)
- New effectiveScopes property returns Set<String> with default fallback
- Updated primeAPIKeyScopes() to cache both server access and scopes
- Added assertScopeAllowed() to check protocol permissions
- withServer() now checks IMAP scope automatically
- sendDraft() explicitly checks SMTP scope
- CLI: post api-key create --scopes imap,smtp
- CLI: post api-key list shows scopes

Scope model:
- imap: All IMAP operations (list, fetch, flag, move, trash, draft, IDLE)
- smtp: Send emails via sendDraft

Backward compatibility:
βœ… Existing API keys (scopes=nil) default to IMAP-only
βœ… Existing keys cannot send until explicitly granted smtp scope
βœ… No breaking changes to API

Testing:
- βœ… swift build -c release
- βœ… swift test (8/8 passing)

Example usage:
# IMAP-only (read, organize, draft)
post api-key create --servers drobnik --scopes imap

# Full access (IMAP + send)
post api-key create --servers drobnik --scopes imap,smtp

# Legacy keys automatically become IMAP-only
Aligns SMTP with IMAP's dual-source credential pattern.

Changes:
- Simplified SMTPConfiguration to only contain optional credentials
- Removed redundant host/port/useTLS fields (now part of credentials)
- Updated resolveSMTPCredentials() to check config first, then keychain
- Removed smtpNotConfigured error (no longer needed)
- sendDraft() infers TLS mode from port (465=TLS, 587/25=STARTTLS)

Credential resolution order:
1. Config: servers.X.smtp.credentials (if present)
2. Keychain: SMTP entry for server X (if present)
3. Error: noSMTPCredentials

Example config:
{
  "servers": {
    "drobnik": {
      "smtp": {
        "credentials": {
          "host": "mail.example.com",
          "port": 587,
          "username": "user@example.com",
          "password": "secret"
        }
      }
    }
  }
}

Or use keychain:
post credential set --server drobnik --smtp

Testing:
- βœ… swift build -c release
- βœ… swift test (8/8 passing)
- βœ… Symmetric with IMAP credential resolution
@odrobnik odrobnik merged commit a73d81c into main Mar 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add 'post send' command to send drafts via SMTP

1 participant