feat: complete code formatting and linting compliance

- Fix all Rust clippy warnings with targeted #[allow] attributes for justified cases
- Implement server-side IMAP SEARCH keyword filtering in Go implementation
- Add graceful fallback from server-side to client-side filtering when IMAP server lacks SEARCH support
- Ensure both implementations use identical filtering logic for consistent results
- Complete comprehensive testing of filtering and attachment handling functionality
- Verify production readiness with proper linting standards for both Go and Rust

🤖 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-05 19:20:22 +02:00
commit 6c387abfbb
13 changed files with 851 additions and 432 deletions

View file

@ -1,5 +1,5 @@
//! CouchDB client integration for mail2couch
//!
//!
//! This module provides a CouchDB client that handles database operations
//! for storing email messages and sync metadata.
@ -58,14 +58,14 @@ impl CouchClient {
match operation().await {
Ok(result) => {
if attempt > 1 {
log::debug!("✅ CouchDB {} successful on attempt {}", operation_name, attempt);
log::debug!("✅ CouchDB {operation_name} successful on attempt {attempt}");
}
return Ok(result);
}
Err(e) => {
// Check if this is a retryable error
let is_retryable = match &e.downcast_ref::<CouchError>() {
Some(CouchError::Http(_)) => true, // Network errors are retryable
Some(CouchError::Http(_)) => true, // Network errors are retryable
Some(CouchError::CouchDb { status, .. }) => {
// Retry on server errors (5xx) but not client errors (4xx)
*status >= 500
@ -74,7 +74,7 @@ impl CouchClient {
};
last_error = Some(e);
if is_retryable && attempt < MAX_RETRIES {
log::warn!(
"🔄 CouchDB {} attempt {} failed, retrying in {}ms: {}",
@ -120,13 +120,13 @@ impl CouchClient {
pub async fn test_connection(&self) -> Result<()> {
let url = format!("{}/", self.base_url);
let mut request = self.client.get(&url);
if let Some((username, password)) = &self.auth {
request = request.basic_auth(username, Some(password));
}
let response = request.send().await?;
if response.status().is_success() {
Ok(())
} else {
@ -143,18 +143,23 @@ impl CouchClient {
let url = format!("{}/{}", self.base_url, db_name);
let mut request = self.client.put(&url);
if let Some((username, password)) = &self.auth {
request = request.basic_auth(username, Some(password));
}
let response = request.send().await?;
match response.status() {
StatusCode::CREATED | StatusCode::ACCEPTED => Ok(()),
status => {
let error_text = response.text().await?;
Err(anyhow!("Failed to create database {}: {} - {}", db_name, status, error_text))
Err(anyhow!(
"Failed to create database {}: {} - {}",
db_name,
status,
error_text
))
}
}
}
@ -163,7 +168,7 @@ impl CouchClient {
pub async fn database_exists(&self, db_name: &str) -> Result<bool> {
let url = format!("{}/{}", self.base_url, db_name);
let mut request = self.client.head(&url);
if let Some((username, password)) = &self.auth {
request = request.basic_auth(username, Some(password));
}
@ -173,14 +178,18 @@ impl CouchClient {
}
/// Store a mail document in CouchDB with optional attachments and retry logic
pub async fn store_mail_document(&self, db_name: &str, mut document: MailDocument) -> Result<String> {
pub async fn store_mail_document(
&self,
db_name: &str,
mut document: MailDocument,
) -> Result<String> {
// Set the document ID if not already set
if document.id.is_none() {
document.set_id();
}
let doc_id = document.id.as_ref().unwrap().clone();
// Check if document already exists to avoid duplicates
if self.document_exists(db_name, &doc_id).await? {
return Ok(doc_id);
@ -190,30 +199,33 @@ impl CouchClient {
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 {
request = request.basic_auth(username, Some(password));
}
let response = request.send().await
.map_err(|e| CouchError::Http(e))?;
let response = request.send().await.map_err(CouchError::Http)?;
match response.status() {
StatusCode::CREATED | StatusCode::ACCEPTED => {
let couch_response: CouchResponse = response.json().await
.map_err(|e| CouchError::Http(e))?;
let couch_response: CouchResponse =
response.json().await.map_err(CouchError::Http)?;
Ok(couch_response.id.unwrap_or_else(|| doc_id.clone()))
}
status => {
let error_text = response.text().await
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
Err(CouchError::CouchDb {
status: status.as_u16(),
message: error_text,
}.into())
}
.into())
}
}
}).await
})
.await
}
/// Store an attachment for a document in CouchDB
@ -232,26 +244,35 @@ impl CouchClient {
// Upload the attachment
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
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)
.body(data.to_vec());
if let Some((username, password)) = &self.auth {
request = request.basic_auth(username, Some(password));
}
let response = request.send().await?;
match response.status() {
StatusCode::CREATED | StatusCode::ACCEPTED => {
let couch_response: CouchResponse = response.json().await?;
Ok(couch_response.rev.unwrap_or_else(|| rev))
Ok(couch_response.rev.unwrap_or(rev))
}
status => {
let error_text = response.text().await?;
Err(anyhow!("Failed to store attachment {}: {} - {}", attachment_name, status, error_text))
Err(anyhow!(
"Failed to store attachment {}: {} - {}",
attachment_name,
status,
error_text
))
}
}
}
@ -261,13 +282,13 @@ impl CouchClient {
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 {
request = request.basic_auth(username, Some(password));
}
let response = request.send().await?;
match response.status() {
StatusCode::OK => {
let doc: Value = response.json().await?;
@ -276,15 +297,24 @@ impl CouchClient {
StatusCode::NOT_FOUND => Ok(None),
status => {
let error_text = response.text().await?;
Err(anyhow!("Failed to get document {}: {} - {}", doc_id, status, error_text))
Err(anyhow!(
"Failed to get document {}: {} - {}",
doc_id,
status,
error_text
))
}
}
}
/// Store sync metadata in CouchDB
pub async fn store_sync_metadata(&self, db_name: &str, metadata: &SyncMetadata) -> Result<String> {
pub async fn store_sync_metadata(
&self,
db_name: &str,
metadata: &SyncMetadata,
) -> Result<String> {
let doc_id = metadata.id.as_ref().unwrap();
// Try to get existing document first to get the revision
let mut metadata_to_store = metadata.clone();
if let Ok(existing) = self.get_sync_metadata(db_name, &metadata.mailbox).await {
@ -294,13 +324,13 @@ impl CouchClient {
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 {
request = request.basic_auth(username, Some(password));
}
let response = request.send().await?;
match response.status() {
StatusCode::CREATED | StatusCode::ACCEPTED => {
let couch_response: CouchResponse = response.json().await?;
@ -308,35 +338,43 @@ impl CouchClient {
}
status => {
let error_text = response.text().await?;
Err(anyhow!("Failed to store sync metadata {}: {} - {}", doc_id, status, error_text))
Err(anyhow!(
"Failed to store sync metadata {}: {} - {}",
doc_id,
status,
error_text
))
}
}
}
/// 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 doc_id = format!("sync_metadata_{mailbox}");
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 {
request = request.basic_auth(username, Some(password));
}
let response = request.send().await?;
match response.status() {
StatusCode::OK => {
let metadata: SyncMetadata = response.json().await?;
Ok(metadata)
}
StatusCode::NOT_FOUND => {
Err(CouchError::NotFound { id: doc_id }.into())
}
StatusCode::NOT_FOUND => Err(CouchError::NotFound { id: doc_id }.into()),
status => {
let error_text = response.text().await?;
Err(anyhow!("Failed to get sync metadata {}: {} - {}", doc_id, status, error_text))
Err(anyhow!(
"Failed to get sync metadata {}: {} - {}",
doc_id,
status,
error_text
))
}
}
}
@ -346,7 +384,7 @@ impl CouchClient {
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 {
request = request.basic_auth(username, Some(password));
}
@ -359,13 +397,13 @@ impl CouchClient {
pub async fn get_database_info(&self, db_name: &str) -> Result<Value> {
let url = format!("{}/{}", self.base_url, db_name);
let mut request = self.client.get(&url);
if let Some((username, password)) = &self.auth {
request = request.basic_auth(username, Some(password));
}
let response = request.send().await?;
match response.status() {
StatusCode::OK => {
let info: Value = response.json().await?;
@ -373,7 +411,12 @@ impl CouchClient {
}
status => {
let error_text = response.text().await?;
Err(anyhow!("Failed to get database info for {}: {} - {}", db_name, status, error_text))
Err(anyhow!(
"Failed to get database info for {}: {} - {}",
db_name,
status,
error_text
))
}
}
}
@ -382,21 +425,24 @@ impl CouchClient {
pub async fn get_mailbox_uids(&self, db_name: &str, mailbox: &str) -> Result<Vec<u32>> {
let url = format!("{}/{}/_all_docs", self.base_url, db_name);
let query_params = [
("startkey", format!("\"{}\"", mailbox)),
("endkey", format!("\"{}\\ufff0\"", mailbox)), // High Unicode character for range end
("startkey", format!("\"{mailbox}\"")),
("endkey", format!("\"{mailbox}\\ufff0\"")), // High Unicode character for range end
("include_docs", "false".to_string()),
];
let mut request = self.client.get(&url).query(&query_params);
if let Some((username, password)) = &self.auth {
request = request.basic_auth(username, Some(password));
}
let response = request.send().await?;
if !response.status().is_success() {
return Err(anyhow!("Failed to query stored messages: {}", response.status()));
return Err(anyhow!(
"Failed to query stored messages: {}",
response.status()
));
}
let result: serde_json::Value = response.json().await?;
@ -406,7 +452,7 @@ impl CouchClient {
for row in rows {
if let Some(id) = row["id"].as_str() {
// Parse UID from document ID format: {mailbox}_{uid}
if let Some(uid_str) = id.strip_prefix(&format!("{}_", mailbox)) {
if let Some(uid_str) = id.strip_prefix(&format!("{mailbox}_")) {
if let Ok(uid) = uid_str.parse::<u32>() {
uids.push(uid);
}
@ -424,36 +470,45 @@ impl CouchClient {
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 {
request = request.basic_auth(username, Some(password));
}
let response = request.send().await?;
if response.status() == StatusCode::NOT_FOUND {
return Ok(()); // Document already doesn't exist
}
let doc: Value = response.json().await?;
let rev = doc["_rev"].as_str()
let rev = doc["_rev"]
.as_str()
.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, encoded_doc_id, rev);
// Now delete the document
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 {
delete_request = delete_request.basic_auth(username, Some(password));
}
let delete_response = delete_request.send().await?;
match delete_response.status() {
StatusCode::OK | StatusCode::ACCEPTED => Ok(()),
status => {
let error_text = delete_response.text().await?;
Err(anyhow!("Failed to delete document {}: {} - {}", doc_id, status, error_text))
Err(anyhow!(
"Failed to delete document {}: {} - {}",
doc_id,
status,
error_text
))
}
}
}
@ -481,4 +536,4 @@ mod tests {
// Note: Additional integration tests would require a running CouchDB instance
// These would be similar to the Go implementation tests
}
}