")
+
+ if include_metadata:
+ rows = _metadata_rows(metadata)
+ if rows:
+ body_parts.append("DocuElevate metadata
")
+ for key, value in rows:
+ body_parts.append(f"{escape(key)}: {escape(value)}
")
+ body_parts.append("
")
+
+ body_parts.append(f'')
+ body_parts.append("")
+ return "".join(body_parts)
+
+
+def _create_evernote_note(file_path: str, metadata: dict[str, Any], task_id: str):
+ auth_token = getattr(settings, "evernote_auth_token", None)
+ if not auth_token:
+ raise ValueError("Evernote auth token is not configured (EVERNOTE_AUTH_TOKEN)")
+
+ note_store, Types = _get_note_store(auth_token)
+
+ filename = os.path.basename(file_path)
+ with open(file_path, "rb") as pdf_file:
+ resource_body = pdf_file.read()
+
+ body_hash = hashlib.md5(resource_body).digest() # noqa: S324 - Evernote API requires MD5 resource hashes.
+ body_hash_hex = hashlib.md5(resource_body).hexdigest() # noqa: S324 - Evernote ENML references MD5 hashes.
+ resource_mime = mimetypes.guess_type(filename)[0] or "application/pdf"
+
+ data = Types.Data()
+ data.size = len(resource_body)
+ data.bodyHash = body_hash
+ data.body = resource_body
+
+ resource = Types.Resource()
+ resource.mime = resource_mime
+ resource.data = data
+ resource.attributes = Types.ResourceAttributes(fileName=filename)
+
+ note = Types.Note()
+ note.title = _note_title(file_path, metadata)
+ note.content = _build_enml(
+ metadata,
+ body_hash_hex,
+ resource_mime,
+ include_metadata=getattr(settings, "evernote_include_metadata", True),
+ )
+ note.resources = [resource]
+
+ notebook_guid = getattr(settings, "evernote_notebook_guid", None)
+ if notebook_guid:
+ note.notebookGuid = notebook_guid
+
+ tag_names = _extract_tags(metadata)
+ if tag_names:
+ note.tagNames = tag_names
+
+ created_note = note_store.createNote(auth_token, note)
+
+ logger.info("[%s] Created Evernote note %s for %s", task_id, getattr(created_note, "guid", None), file_path)
+ return created_note
+
+
+@celery.task(base=UploadTaskWithRetry, bind=True)
+def upload_to_evernote(self, file_path: str, file_id: int = None):
+ """
+ Upload a document to Evernote by creating a note with metadata and a PDF attachment.
+
+ Args:
+ file_path: Path to the PDF file to upload
+ file_id: Optional file ID to associate with logs
+ """
+ task_id = self.request.id
+ filename = os.path.basename(file_path)
+ logger.info("[%s] Starting Evernote upload: %s", task_id, file_path)
+ log_task_progress(
+ task_id, "upload_to_evernote", "in_progress", f"Uploading to Evernote: {filename}", file_id=file_id
+ )
+
+ if not os.path.exists(file_path):
+ error_msg = f"File not found: {file_path}"
+ logger.error("[%s] %s", task_id, error_msg)
+ log_task_progress(task_id, "upload_to_evernote", "failure", error_msg, file_id=file_id)
+ raise FileNotFoundError(error_msg)
+
+ if not getattr(settings, "evernote_auth_token", None):
+ error_msg = "Evernote auth token is not configured (EVERNOTE_AUTH_TOKEN)"
+ logger.error("[%s] %s", task_id, error_msg)
+ log_task_progress(task_id, "upload_to_evernote", "failure", error_msg, file_id=file_id)
+ raise ValueError(error_msg)
+
+ try:
+ metadata = _load_metadata(file_path)
+ created_note = _create_evernote_note(file_path, metadata, task_id)
+ except Exception as exc:
+ error_msg = f"Failed to upload to Evernote: {exc}"
+ logger.error("[%s] %s", task_id, error_msg)
+ log_task_progress(task_id, "upload_to_evernote", "failure", error_msg, file_id=file_id)
+ raise
+
+ note_guid = getattr(created_note, "guid", None)
+ log_task_progress(task_id, "upload_to_evernote", "success", f"Uploaded to Evernote: {note_guid}", file_id=file_id)
+ return {
+ "status": "Completed",
+ "file_path": file_path,
+ "evernote_note_guid": note_guid,
+ "evernote_title": getattr(created_note, "title", None),
+ "evernote_notebook_guid": getattr(created_note, "notebookGuid", None),
+ }
diff --git a/app/utils/config_validator/providers.py b/app/utils/config_validator/providers.py
index 91383438..871b87a5 100644
--- a/app/utils/config_validator/providers.py
+++ b/app/utils/config_validator/providers.py
@@ -174,6 +174,21 @@ def get_provider_status() -> dict[str, dict[str, object]]:
},
}
+ providers["Evernote"] = {
+ "name": "Evernote",
+ "icon": "fa-brands fa-evernote",
+ "configured": bool(getattr(settings, "evernote_auth_token", None)),
+ "enabled": getattr(settings, "evernote_enabled", True),
+ "description": "Create Evernote notes with document metadata and PDF attachments",
+ "details": {
+ "auth_token": mask_sensitive_value(getattr(settings, "evernote_auth_token", None)),
+ "sandbox": getattr(settings, "evernote_sandbox", False),
+ "notebook_guid": getattr(settings, "evernote_notebook_guid", "Not set"),
+ "default_tags": getattr(settings, "evernote_default_tags", "Not set"),
+ "include_metadata": getattr(settings, "evernote_include_metadata", True),
+ },
+ }
+
# Add FTP configuration to providers
providers["FTP Storage"] = {
"name": "FTP Storage",
diff --git a/app/utils/config_validator/validators.py b/app/utils/config_validator/validators.py
index 6ef8d52b..91a28634 100644
--- a/app/utils/config_validator/validators.py
+++ b/app/utils/config_validator/validators.py
@@ -145,6 +145,12 @@ def validate_storage_configs() -> dict[str, list[str]]:
email_issues.append("DEST_EMAIL_DEFAULT_RECIPIENT is not configured")
issues["email"] = email_issues
+ # Validate Evernote
+ evernote_issues = []
+ if not getattr(settings, "evernote_auth_token", None):
+ evernote_issues.append("EVERNOTE_AUTH_TOKEN is not configured")
+ issues["evernote"] = evernote_issues
+
# Validate S3
s3_issues = []
if not getattr(settings, "s3_bucket_name", None):
diff --git a/app/views/files.py b/app/views/files.py
index 3d44efcc..3c47dc66 100644
--- a/app/views/files.py
+++ b/app/views/files.py
@@ -717,6 +717,7 @@ def _compute_processing_flow(logs, pipeline_steps=None):
"upload_to_ftp": "FTP Storage",
"upload_to_sftp": "SFTP Storage",
"upload_to_email": "Email",
+ "upload_to_evernote": "Evernote",
"queue_dropbox": "Dropbox",
"queue_nextcloud": "Nextcloud",
"queue_paperless": "Paperless-ngx",
@@ -727,6 +728,7 @@ def _compute_processing_flow(logs, pipeline_steps=None):
"queue_ftp": "FTP Storage",
"queue_sftp": "SFTP Storage",
"queue_email": "Email",
+ "queue_evernote": "Evernote",
}
# Create a map of step names to their log entries
diff --git a/docs/ConfigurationGuide.md b/docs/ConfigurationGuide.md
index 39d906dd..a56e9af9 100644
--- a/docs/ConfigurationGuide.md
+++ b/docs/ConfigurationGuide.md
@@ -1330,6 +1330,19 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
| `DEST_EMAIL_SENDER` | From address for delivered documents (e.g., `"DocuElevate Delivery