Troubleshooting
This page covers the most common issues encountered when running PRX-Email, along with their causes and solutions.
OAuth Token Expired
Symptoms: Operations fail with Provider error code and a message about expired tokens.
Possible Causes:
- OAuth access token has expired and no refresh provider is configured
- The
*_OAUTH_EXPIRES_ATenvironment variable contains a stale timestamp - The refresh provider is returning errors
Solutions:
- Verify token expiry timestamps:
echo $PRX_EMAIL_IMAP_OAUTH_EXPIRES_AT
echo $PRX_EMAIL_SMTP_OAUTH_EXPIRES_AT
# These should be Unix timestamps in the future- Manually reload tokens from environment:
// Set fresh tokens
std::env::set_var("PRX_EMAIL_IMAP_OAUTH_TOKEN", "new-token");
std::env::set_var("PRX_EMAIL_SMTP_OAUTH_TOKEN", "new-token");
// Reload
plugin.reload_auth_from_env("PRX_EMAIL");- Implement a refresh provider for automatic token renewal:
let plugin = EmailPlugin::new_with_config(repo, config)
.with_refresh_provider(Box::new(my_refresh_provider));- Re-run the Outlook bootstrap script to get fresh tokens:
CLIENT_ID='...' TENANT='...' REDIRECT_URI='...' \
./scripts/outlook_oauth_bootstrap.shTIP
PRX-Email attempts to refresh tokens 60 seconds before they expire. If your tokens expire faster than your sync interval, ensure the refresh provider is connected.
IMAP Sync Fails
Symptoms: sync() returns a Network error, or the sync runner reports failures.
Possible Causes:
- Incorrect IMAP server hostname or port
- Network connectivity issues
- Authentication failure (wrong password or expired OAuth token)
- IMAP server rate limiting
Solutions:
- Verify connectivity to the IMAP server:
openssl s_client -connect imap.example.com:993 -quiet- Check the transport configuration:
// Ensure host and port are correct
println!("IMAP host: {}", config.imap.host);
println!("IMAP port: {}", config.imap.port);- Verify authentication mode:
// Must have exactly one set
assert!(config.imap.auth.password.is_some() ^ config.imap.auth.oauth_token.is_some());- Check sync runner backoff state. After repeated failures, the scheduler applies exponential backoff. Temporarily reset by using a far-future
now_ts:
let report = plugin.run_sync_runner(&jobs, now + 86400, &config);- Check structured logs for detailed error information:
# Look for sync-related structured logs
grep "prx_email.*sync" /path/to/logsSMTP Send Fails
Symptoms: send() returns an ApiResponse with ok: false and a Network or Provider error.
Possible Causes:
- Incorrect SMTP server hostname or port
- Authentication failure
- Recipient address rejected by the provider
- Rate limiting or sending quota exceeded
Solutions:
- Check the outbox status:
let outbox = plugin.get_outbox(outbox_id)?;
if let Some(msg) = outbox {
println!("Status: {}", msg.status);
println!("Retries: {}", msg.retries);
println!("Last error: {:?}", msg.last_error);
println!("Next attempt: {}", msg.next_attempt_at);
}- Verify SMTP configuration:
// Check auth mode
println!("Auth: password={}, oauth={}",
config.smtp.auth.password.is_some(),
config.smtp.auth.oauth_token.is_some());Check for validation errors. The send API rejects:
- Empty
to,subject, orbody_text - Disabled
email_sendfeature flag - Invalid email addresses
- Empty
Test with simulated failure to verify your error handling:
use prx_email::plugin::SendFailureMode;
let response = plugin.send(SendEmailRequest {
// ... fields ...
failure_mode: Some(SendFailureMode::Network), // Simulate failure
});Outbox Stuck in "sending" State
Symptoms: Outbox records have status = 'sending' but the process crashed before finalization.
Cause: The process crashed after claiming the outbox record but before finalizing it as sent or failed.
Solution: Manually recover stuck records via SQL:
-- Identify stuck rows (threshold: 15 minutes)
SELECT id, account_id, updated_at
FROM outbox
WHERE status = 'sending' AND updated_at < strftime('%s','now') - 900;
-- Recover to failed and schedule retry
UPDATE outbox
SET status = 'failed',
last_error = 'recovered_from_stuck_sending',
next_attempt_at = strftime('%s','now') + 30,
updated_at = strftime('%s','now')
WHERE status = 'sending' AND updated_at < strftime('%s','now') - 900;Attachment Rejected
Symptoms: Send fails with "attachment exceeds size limit" or "attachment content type is not allowed".
Solutions:
- Check the attachment policy:
let policy = &config.attachment_policy;
println!("Max size: {} bytes", policy.max_size_bytes);
println!("Allowed types: {:?}", policy.allowed_content_types);Verify the file size is within the limit (default: 25 MiB).
Add the MIME type to the allowed list if it is safe:
policy.allowed_content_types.insert("application/vnd.ms-excel".to_string());- For path-based attachments, ensure the file path is under the configured attachment storage root. Paths containing
../or symlinks that resolve outside the root are rejected.
Feature Disabled Error
Symptoms: Operations return FeatureDisabled error code.
Cause: The feature flag for the requested operation is not enabled for the account.
Solution:
// Check current state
let enabled = plugin.is_feature_enabled(account_id, "email_send")?;
println!("email_send enabled: {}", enabled);
// Enable the feature
plugin.set_account_feature(account_id, "email_send", true, now)?;
// Or set the global default
plugin.set_feature_default("email_send", true, now)?;SQLite Database Errors
Symptoms: Operations fail with Storage error code.
Possible Causes:
- Database file is locked by another process
- Disk is full
- Database file is corrupted
- Migrations have not been run
Solutions:
- Run migrations:
let store = EmailStore::open("./email.db")?;
store.migrate()?;- Check for locked database. Only one write connection can be active at a time. Increase the busy timeout:
let config = StoreConfig {
busy_timeout_ms: 30_000, // 30 seconds
..StoreConfig::default()
};- Check disk space:
df -h .- Repair or recreate if the database is corrupted:
# Back up the existing database
cp email.db email.db.bak
# Check integrity
sqlite3 email.db "PRAGMA integrity_check;"
# If corrupt, export and reimport
sqlite3 email.db ".dump" | sqlite3 email_new.dbWASM Plugin Issues
Network Guard Error
Symptoms: WASM-hosted email operations return EMAIL_NETWORK_GUARD error.
Cause: The network safety switch is not enabled.
Solution:
export PRX_EMAIL_ENABLE_REAL_NETWORK=1Host Capability Unavailable
Symptoms: Operations return EMAIL_HOST_CAPABILITY_UNAVAILABLE.
Cause: The host runtime does not provide the email capability. This occurs when running outside the WASM context.
Solution: Ensure the PRX runtime is configured to provide email host-calls to the plugin.
Sync Runner Keeps Skipping Jobs
Symptoms: The sync runner reports attempted: 0 even though jobs are configured.
Cause: All jobs are in backoff due to previous failures.
Solutions:
Check the failure backoff state by examining structured logs.
Verify network reachability and IMAP authentication before re-running.
Reset backoff by using a far-future timestamp:
let report = plugin.run_sync_runner(&jobs, now + 86400, &default_config);High Send Failure Rate
Symptoms: Metrics show a high send_failures count.
Solutions:
- Inspect structured logs filtered by
run_idanderror_code:
grep "prx_email.*send_failed" /path/to/logsCheck SMTP auth mode. Ensure exactly one of password or oauth_token is set.
Validate provider availability before enabling broad rollout.
Check metrics:
let metrics = plugin.metrics_snapshot();
println!("Send failures: {}", metrics.send_failures);
println!("Retry count: {}", metrics.retry_count);Getting Help
If none of the above solutions resolve your issue:
- Check existing issues: github.com/openprx/prx_email/issues
- File a new issue with:
- PRX-Email version (check
Cargo.toml) - Rust toolchain version (
rustc --version) - Relevant structured log output
- Steps to reproduce
- PRX-Email version (check
Next Steps
- Configuration Reference -- Review all settings
- OAuth Authentication -- Resolve OAuth-specific issues
- SQLite Storage -- Database maintenance and recovery