fix: resolve document ID conflicts with URL encoding

- Fixed document ID conflicts caused by unencoded slashes in mailbox names
- Added URL encoding for all document IDs used in CouchDB REST API calls
- Mailbox names with slashes (e.g., 'Work/Projects') now create proper document IDs
- Resolves issue where 'Work/Projects_1' was incorrectly stored as document 'Work' with attachment 'Projects_1'
- Added urlencoding dependency for proper URL-safe document ID handling

All messages now store successfully without conflicts across all mailboxes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-03 14:05:03 +02:00
commit fbc8ebbbdf
2 changed files with 20 additions and 9 deletions

View file

@ -52,6 +52,9 @@ dirs = "5.0"
# Pattern matching for folder filters
glob = "0.3"
# URL encoding for document IDs
urlencoding = "2.1"
[dev-dependencies]
# Testing utilities
tokio-test = "0.4"

View file

@ -187,7 +187,8 @@ impl CouchClient {
}
self.retry_operation("store_mail_document", || async {
let url = format!("{}/{}/{}", self.base_url, db_name, doc_id);
let encoded_doc_id = urlencoding::encode(&doc_id);
let url = format!("{}/{}/{}", self.base_url, db_name, encoded_doc_id);
let mut request = self.client.put(&url).json(&document);
if let Some((username, password)) = &self.auth {
@ -229,7 +230,9 @@ impl CouchClient {
let rev = doc_response.ok_or_else(|| anyhow!("Document {} not found", doc_id))?;
// Upload the attachment
let url = format!("{}/{}/{}/{}?rev={}", self.base_url, db_name, doc_id, attachment_name, rev);
let encoded_doc_id = urlencoding::encode(doc_id);
let encoded_attachment_name = urlencoding::encode(attachment_name);
let url = format!("{}/{}/{}/{}?rev={}", self.base_url, db_name, encoded_doc_id, encoded_attachment_name, rev);
let mut request = self.client
.put(&url)
.header("Content-Type", content_type)
@ -255,7 +258,8 @@ impl CouchClient {
/// Get document revision
async fn get_document_rev(&self, db_name: &str, doc_id: &str) -> Result<Option<String>> {
let url = format!("{}/{}/{}", self.base_url, db_name, doc_id);
let encoded_doc_id = urlencoding::encode(doc_id);
let url = format!("{}/{}/{}", self.base_url, db_name, encoded_doc_id);
let mut request = self.client.get(&url);
if let Some((username, password)) = &self.auth {
@ -287,7 +291,8 @@ impl CouchClient {
metadata_to_store.rev = existing.rev;
}
let url = format!("{}/{}/{}", self.base_url, db_name, doc_id);
let encoded_doc_id = urlencoding::encode(doc_id);
let url = format!("{}/{}/{}", self.base_url, db_name, encoded_doc_id);
let mut request = self.client.put(&url).json(&metadata_to_store);
if let Some((username, password)) = &self.auth {
@ -311,7 +316,8 @@ impl CouchClient {
/// Get sync metadata for a mailbox
pub async fn get_sync_metadata(&self, db_name: &str, mailbox: &str) -> Result<SyncMetadata> {
let doc_id = format!("sync_metadata_{}", mailbox);
let url = format!("{}/{}/{}", self.base_url, db_name, doc_id);
let encoded_doc_id = urlencoding::encode(&doc_id);
let url = format!("{}/{}/{}", self.base_url, db_name, encoded_doc_id);
let mut request = self.client.get(&url);
if let Some((username, password)) = &self.auth {
@ -337,7 +343,8 @@ impl CouchClient {
/// Check if a document exists
pub async fn document_exists(&self, db_name: &str, doc_id: &str) -> Result<bool> {
let url = format!("{}/{}/{}", self.base_url, db_name, doc_id);
let encoded_doc_id = urlencoding::encode(doc_id);
let url = format!("{}/{}/{}", self.base_url, db_name, encoded_doc_id);
let mut request = self.client.head(&url);
if let Some((username, password)) = &self.auth {
@ -414,7 +421,8 @@ impl CouchClient {
/// Delete a document (used in sync mode for deleted messages)
pub async fn delete_document(&self, db_name: &str, doc_id: &str) -> Result<()> {
// First get the document to get its revision
let url = format!("{}/{}/{}", self.base_url, db_name, doc_id);
let encoded_doc_id = urlencoding::encode(doc_id);
let url = format!("{}/{}/{}", self.base_url, db_name, encoded_doc_id);
let mut request = self.client.get(&url);
if let Some((username, password)) = &self.auth {
@ -432,7 +440,7 @@ impl CouchClient {
.ok_or_else(|| anyhow!("Document {} has no _rev field", doc_id))?;
// Now delete the document
let delete_url = format!("{}/{}/{}?rev={}", self.base_url, db_name, doc_id, rev);
let delete_url = format!("{}/{}/{}?rev={}", self.base_url, db_name, encoded_doc_id, rev);
let mut delete_request = self.client.delete(&delete_url);
if let Some((username, password)) = &self.auth {