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:
parent
436276f0ef
commit
6c387abfbb
13 changed files with 851 additions and 432 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue