Email fetcher: one.com → Migadu migration

Email Fetcher Migration: one.com → Migadu

MC Task: #100395
Migration Date: 2026-05-12 14:30-15:00 UTC
Verification: 12/12 atomic claims PASS (15:05 UTC)
Status: Production cutover complete

Overview

On 2026-05-10 20:20 UTC, the CEO registered Migadu email hosting and switched MX records for alai.no and basicconsulting.no to Migadu (priority 10/20), retaining one.com as fallback (priority 100). The email-agent daemon (~/system/tools/email-agent.js) and mail-native.js IMAP client were still configured for one.com IMAP, making mail routed to Migadu invisible to John's email processing pipeline. On 2026-05-12, John executed the migration cutover: provisioned 4 new Migadu mailboxes via Admin API, rotated Bitwarden credentials, reconfigured himalaya CLI and mail-native.js to use imap.migadu.com:993 + smtp.migadu.com:465, and deployed a workaround for himalaya 1.1.0 PLAIN SASL incompatibility by forcing ImapFlow fallback (HIMALAYA_DISABLED=1 env var).

Affected Components

Migadu Mailbox Provisioning

Migadu mailboxes are created via Admin API using the API key from Bitwarden item migadu keyy.

Create mailbox via API

# Get Migadu API credentials
API_KEY=$(bw get password 'migadu keyy' --session $(cat /tmp/bw-session))
DOMAIN="alai.no"  # or basicconsulting.no
LOCAL_PART="john"
PASSWORD=$(openssl rand -base64 24)

# Create mailbox
curl -X POST "https://api.migadu.com/v1/domains/${DOMAIN}/mailboxes" \
  -u "admin@alai.no:${API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "local_part": "'"${LOCAL_PART}"'",
    "password": "'"${PASSWORD}"'",
    "may_send": true,
    "may_receive": true,
    "may_access_imap": true,
    "may_access_pop3": true,
    "may_access_managesieve": true
  }'

# Verify creation
curl "https://api.migadu.com/v1/domains/${DOMAIN}/mailboxes/${LOCAL_PART}" \
  -u "admin@alai.no:${API_KEY}"

Store credentials in Bitwarden

# Create Bitwarden item with naming convention
echo "{
  \"organizationId\": null,
  \"folderId\": null,
  \"type\": 1,
  \"name\": \"Migadu — ${LOCAL_PART}@${DOMAIN}\",
  \"login\": {
    \"username\": \"${LOCAL_PART}@${DOMAIN}\",
    \"password\": \"${PASSWORD}\",
    \"uris\": [
      { \"match\": null, \"uri\": \"imap.migadu.com\" },
      { \"match\": null, \"uri\": \"smtp.migadu.com\" }
    ]
  }
}" | bw encode | bw create item --session $(cat /tmp/bw-session)

Evidence: See /Users/makinja/system/state/evidence/migadu-mailbox-create-20260512T083653Z.log for actual execution output of 4 mailboxes created on 2026-05-12.

Bitwarden Credential Structure

Naming convention: Migadu — <email address>

This convention is hardcoded in ~/system/tools/mail-native.js VAULT_NAMES mapping:

const VAULT_NAMES = {
  john: 'Migadu — john@basicconsulting.no',
  info: 'Migadu — info@basicconsulting.no',
  alai: 'Migadu — john@alai.no',
  alem: 'Migadu — alem@alai.no',
  dev: 'Migadu — dev@alai.no',
  gmail: 'Gmail — alembasic@gmail.com'
};

List all Migadu credentials:

bw list items --search 'Migadu —' --session $(cat /tmp/bw-session) | jq -r '.[] | "\(.name) (\(.id))"'

End-to-End Verification

Use Python imaplib to verify IMAP login and mailbox access:

#!/usr/bin/env python3
import imaplib
import json
import subprocess

def test_imap(email):
    # Fetch password from Bitwarden
    vault_name = f"Migadu — {email}"
    bw_session = open('/tmp/bw-session').read().strip()
    password = subprocess.check_output(
        ['bw', 'get', 'password', vault_name, '--session', bw_session],
        text=True
    ).strip()
    
    # Connect to Migadu IMAP
    imap = imaplib.IMAP4_SSL('imap.migadu.com', 993)
    imap.login(email, password)
    status, messages = imap.select('INBOX')
    
    if status == 'OK':
        msg_count = int(messages[0])
        print(f"✓ {email}: INBOX OK, {msg_count} messages")
    else:
        print(f"✗ {email}: SELECT INBOX failed")
    
    imap.logout()

if __name__ == '__main__':
    accounts = [
        'alem@alai.no',
        'john@alai.no',
        'john@basicconsulting.no',
        'info@basicconsulting.no',
        'dev@alai.no'
    ]
    for acc in accounts:
        test_imap(acc)

Expected output:

✓ alem@alai.no: INBOX OK, 881 messages
✓ john@alai.no: INBOX OK, 0 messages
✓ john@basicconsulting.no: INBOX OK, 13 messages
✓ info@basicconsulting.no: INBOX OK, 0 messages
✓ dev@alai.no: INBOX OK, 0 messages

Evidence: Verifier executed equivalent IMAP4_SSL login probes on 2026-05-12 15:05 UTC — claim C3 PASS (see /Users/makinja/system/state/evidence/mc-100395-verifier-verdict-20260512.md).

Rollback Procedure

If Migadu migration must be reverted (e.g., service outage, credential issues), follow these steps:

  1. Stop email-agent daemon:
    launchctl unload ~/Library/LaunchAgents/com.john.email-agent.plist
    
  2. Restore himalaya config to one.com:
    cp ~/.config/himalaya/config.toml ~/.config/himalaya/config.toml.migadu-backup-$(date +%Y%m%d-%H%M%S)
    cp ~/.config/himalaya/config.toml.one-com-backup-20260512-163802 ~/.config/himalaya/config.toml
    
  3. Revert mail-native.js (verify before running):
    cd ~/system/tools
    git diff mail-native.js  # Review changes
    git checkout HEAD -- mail-native.js  # Revert to pre-migration state
    
  4. Remove HIMALAYA_DISABLED from LaunchAgent:
    cp ~/Library/LaunchAgents/com.john.email-agent.plist ~/Library/LaunchAgents/com.john.email-agent.plist.bak-$(date +%Y%m%d-%H%M%S)
    # Edit plist to remove HIMALAYA_DISABLED env var:
    plutil -replace EnvironmentVariables.HIMALAYA_DISABLED -string "" ~/Library/LaunchAgents/com.john.email-agent.plist
    # Or manually edit and remove the key/value pair
    
  5. Update Bitwarden vault names in code (if needed):

    If mail-native.js git revert doesn't restore old BW item names, manually edit VAULT_NAMES back to Email - <addr> pattern.

  6. Restart daemon:
    launchctl load ~/Library/LaunchAgents/com.john.email-agent.plist
    
  7. Verify connection to one.com:
    tail -f ~/system/logs/email-agent.log | grep -E "Connected|ERROR"
    
    Expect lines like: Connected to john (john@basicconsulting.no) within 60 seconds.

Rollback time estimate: 5-10 minutes (assuming backups are intact).

Known Issue: himalaya 1.1.0 PLAIN SASL vs Migadu

Symptom: himalaya CLI 1.1.0 fails IMAP login to imap.migadu.com:993 with error:

Error: cannot parse envelope at line 1 near column 1
Kind: MalformedMessage

Root cause: himalaya 1.1.0 attempts PLAIN SASL authentication, which Migadu's IMAP server rejects or mishandles (verify before re-running). This is a known incompatibility between himalaya's IMAP library and Migadu's Dovecot configuration.

Workaround: Force email-agent.js to skip himalaya and use ImapFlow (native Node.js IMAP client) by setting environment variable:

<key>EnvironmentVariables</key>
<dict>
  <key>HIMALAYA_DISABLED</key>
  <string>1</string>
</dict>

in ~/Library/LaunchAgents/com.john.email-agent.plist.

Evidence: Email-agent.js log on 2026-05-12 14:48:12 shows:

{"timestamp":"2026-05-12T14:48:12.896Z","service":"email-agent","level":"info","message":"[WARN] himalaya disabled for john, falling back to legacy unseen fetch"}

All 6 mailboxes connected successfully via ImapFlow (claims C9/C10 PASS).

Long-term fix: File upstream issue with himalaya maintainers or test downgrade to himalaya 1.0.x. Track in separate MC task.

Migration Timeline

Timestamp (UTC)Event
2026-05-10 20:20CEO registered Migadu, MX records switched
2026-05-12 08:364 mailboxes provisioned via Migadu API (post, dev, info, john@basicconsulting)
2026-05-12 14:37john@alai.no mailbox created
2026-05-12 14:48email-agent cutover: himalaya disabled, ImapFlow connected to 5 Migadu + 1 Gmail
2026-05-12 14:51First mail cycle after cutover: 3 new emails classified via Ollama
2026-05-12 15:05Verifier subagent: 12/12 claims PASS

Post-Migration State

References


Revision #2
Created 2026-05-12 15:09:16 UTC by John
Updated 2026-06-14 20:03:10 UTC by John