# Radicale CardDAV Sync Integration This application now automatically syncs contacts to a Radicale CardDAV server whenever contacts are created, updated, or deleted. ## Features - ✅ **Automatic Sync** - Contacts are automatically synced when created or updated - ✅ **Automatic Deletion** - Contacts are removed from Radicale when soft/hard deleted - ✅ **Non-Blocking** - Sync happens asynchronously without slowing down the API - ✅ **Optional** - Sync is disabled by default, enable by configuring environment variables - ✅ **VCARD 3.0** - Generates standard VCARD format with full contact details ## Setup ### 1. Configure Environment Variables Add these to your `.env.local` or production environment: ```bash RADICALE_URL=http://localhost:5232 RADICALE_USERNAME=your_username RADICALE_PASSWORD=your_password ``` **Note:** If these variables are not set, sync will be disabled and the app will work normally. ### 2. Radicale Server Setup Make sure your Radicale server: - Is accessible from your application server - Has a user created with the credentials you configured - Has a contacts collection at: `{username}/contacts/` ### 3. One-Time Initial Sync To sync all existing contacts to Radicale: ```bash node export-contacts-to-radicale.mjs ``` This script will: - Prompt for Radicale URL, username, and password - Export all active contacts as VCARDs - Upload them to your Radicale server ## How It Works ### When Creating a Contact ```javascript // POST /api/contacts const contact = createContact(data); // Sync to Radicale asynchronously (non-blocking) syncContactAsync(contact); return NextResponse.json(contact); ``` ### When Updating a Contact ```javascript // PUT /api/contacts/[id] const contact = updateContact(contactId, data); // Sync updated contact to Radicale syncContactAsync(contact); return NextResponse.json(contact); ``` ### When Deleting a Contact ```javascript // DELETE /api/contacts/[id] deleteContact(contactId); // Delete from Radicale asynchronously deleteContactAsync(contactId); return NextResponse.json({ message: "Contact deleted" }); ``` ## VCARD Format Each contact is exported with the following fields: - **UID**: `contact-{id}@panel-app` - **FN/N**: Full name and structured name - **ORG**: Company - **TITLE**: Position/Title - **TEL**: Phone numbers (multiple supported - first as WORK, others as CELL) - **EMAIL**: Email address - **NOTE**: Contact type + notes - **CATEGORIES**: Based on contact type (Projekty, Wykonawcy, Urzędy, etc.) - **REV**: Last modified timestamp ## VCARD Storage Path VCARDs are stored at: ``` {RADICALE_URL}/{RADICALE_USERNAME}/contacts/contact-{id}.vcf ``` Example: ``` http://localhost:5232/admin/contacts/contact-123.vcf ``` ## 🔧 Troubleshooting ### Common Issues **Problem: Sync not working / contacts not appearing in Radicale** **Check 1: Environment Variables** ```bash # Verify variables are set echo $RADICALE_URL echo $RADICALE_USERNAME # Don't echo password for security ``` **Check 2: Radicale Server Connectivity** ```bash # Test server is reachable curl -I http://your-radicale-server:5232 # Test authentication curl -u username:password http://your-radicale-server:5232/username/contacts/ ``` **Check 3: Application Logs** Look for sync messages in your application console: ``` ✅ Synced contact 123 to Radicale ❌ Failed to sync contact 456 to Radicale: 401 - Unauthorized ``` --- **Problem: 401 Unauthorized errors** - **Cause**: Invalid credentials or user doesn't exist - **Solution**: - Verify `RADICALE_USERNAME` and `RADICALE_PASSWORD` - Ensure user exists in Radicale - Check Radicale authentication method (basic auth vs htpasswd) --- **Problem: 404 Not Found errors** - **Cause**: Contacts collection doesn't exist - **Solution**: - Create collection in Radicale: `/{username}/contacts/` - Verify collection URL matches `RADICALE_URL` - Check Radicale collection rights and permissions --- **Problem: Network timeout or connection refused** - **Cause**: Radicale server not accessible from app server - **Solution**: - Check firewall rules - Verify Radicale is running: `systemctl status radicale` - Test with curl from app server - If using Docker, ensure network connectivity --- **Problem: Contacts created but not syncing** - **Cause**: Environment variables not loaded or sync disabled - **Solution**: - Restart application after setting env vars - Check `.env` or `.env.local` file exists - Verify Next.js loaded environment: check server startup logs - Test with manual export script: `node export-contacts-to-radicale.mjs` --- **Problem: Duplicate contacts in Radicale** - **Cause**: Re-running export script or UID conflicts - **Solution**: - UIDs are unique: `contact-{id}@panel-app` - Existing contacts are overwritten on update - Delete duplicates manually in Radicale if needed --- **Problem: VCARD format errors in Radicale** - **Cause**: Invalid characters or incomplete data - **Solution**: - Check contact has at least name field - Special characters in names are escaped - Phone/email fields are optional - Review contact data for completeness --- ### Monitoring Sync Status **Enable Detailed Logging** Edit `src/lib/radicale-sync.js` to increase logging verbosity: ```javascript // Add more console.log statements console.log('Syncing contact:', contact); console.log('VCARD:', vcard); console.log('Response:', await response.text()); ``` **Check Radicale Server Logs** ```bash # Typical log location tail -f /var/log/radicale/radicale.log # Or check systemd journal journalctl -u radicale -f ``` **Manual Sync Test** Test individual contact sync: ```bash # Use the export script for a single contact node export-contacts-to-radicale.mjs # Select specific contact when prompted ``` --- ### Disable Sync Temporarily Comment out environment variables to disable sync without removing configuration: ```bash # .env.local # RADICALE_URL=http://localhost:5232 # RADICALE_USERNAME=admin # RADICALE_PASSWORD=secret ``` Application will function normally without sync enabled. --- ### Manual Sync Endpoint For manual sync control, you can trigger sync via API: ```bash # Sync specific contact POST /api/contacts/{id}/sync # Response { "success": true, "message": "Contact synced to Radicale" } ``` --- ### Error Codes Reference | Code | Meaning | Solution | |------|---------|----------| | 401 | Unauthorized | Check credentials | | 403 | Forbidden | Verify user has write permissions | | 404 | Not Found | Create contacts collection | | 409 | Conflict | UID collision (rare) | | 500 | Server Error | Check Radicale server logs | | ECONNREFUSED | Connection Refused | Server not reachable | | ETIMEDOUT | Timeout | Network/firewall issue | --- ## 📋 Configuration Reference ### Required Environment Variables ```bash RADICALE_URL=http://localhost:5232 RADICALE_USERNAME=your_username RADICALE_PASSWORD=your_password ``` ### Default Settings - **Collection Path**: `/{username}/contacts/` - **VCARD Version**: 3.0 - **UID Format**: `contact-{id}@panel-app` - **Sync Mode**: Asynchronous (non-blocking) - **Retry Logic**: None (fire-and-forget) --- ## 📂 Files Reference | File | Purpose | |------|---------| | `src/lib/radicale-sync.js` | Core sync logic, VCARD generation | | `src/app/api/contacts/route.js` | Create sync trigger | | `src/app/api/contacts/[id]/route.js` | Update/delete sync triggers | | `export-contacts-to-radicale.mjs` | Bulk export utility | --- ## 🔒 Security Best Practices ✅ **Do's:** - Use HTTPS for production Radicale servers - Store credentials in environment variables (never in code) - Use strong, unique passwords - Limit Radicale user permissions to contacts collection only - Regularly rotate credentials - Use separate credentials per environment (dev/staging/prod) ❌ **Don'ts:** - Don't commit credentials to git - Don't use HTTP in production - Don't share credentials between environments - Don't log passwords or sensitive data - Don't grant unnecessary permissions --- ## 🚀 Advanced Configuration ### Custom Collection Path Modify `src/lib/radicale-sync.js`: ```javascript const baseUrl = `${process.env.RADICALE_URL}/${process.env.RADICALE_USERNAME}/my-custom-collection/`; ``` ### Batch Sync Operations For large-scale sync (future enhancement): ```javascript // Collect contacts, then sync in batches const batchSize = 50; // Implement batch logic ``` ### Webhook Integration Future: Trigger webhooks on sync events: ```javascript // POST to webhook URL on sync success/failure fetch(WEBHOOK_URL, { method: 'POST', body: JSON.stringify({ event: 'contact_synced', contact_id: id }) }); ``` --- **See Also**: [Contacts System](CONTACTS_SYSTEM.md) | [Main README](../../README.md#-cardav-integration-radicale) | [API Documentation](../../README.md#contacts)