basically refactored the email viewer

This commit is contained in:
Tim Bendt
2025-05-14 15:11:24 -06:00
parent 5c9ad69309
commit fc57e201a2
20 changed files with 1348 additions and 575 deletions

7
apis/__init__.py Normal file
View File

@@ -0,0 +1,7 @@
"""
APIs package for the GTD Terminal Tools project.
This package contains modules for interacting with various external services like:
- Himalaya email client
- Taskwarrior task manager
"""

21
apis/himalaya/__init__.py Normal file
View File

@@ -0,0 +1,21 @@
"""
Himalaya API module for interacting with the Himalaya email client.
"""
from apis.himalaya.client import (
list_envelopes,
list_accounts,
list_folders,
delete_message,
archive_message,
get_message_content,
)
__all__ = [
"list_envelopes",
"list_accounts",
"list_folders",
"delete_message",
"archive_message",
"get_message_content",
]

169
apis/himalaya/client.py Normal file
View File

@@ -0,0 +1,169 @@
import asyncio
import json
import logging
from typing import Tuple, List, Dict, Any, Optional, Union
async def list_envelopes(limit: int = 9999) -> Tuple[List[Dict[str, Any]], bool]:
"""
Retrieve a list of email envelopes using the Himalaya CLI.
Args:
limit: Maximum number of envelopes to retrieve
Returns:
Tuple containing:
- List of envelope dictionaries
- Success status (True if operation was successful)
"""
try:
process = await asyncio.create_subprocess_shell(
f"himalaya envelope list -o json -s {limit}",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
envelopes = json.loads(stdout.decode())
return envelopes, True
else:
logging.error(f"Error listing envelopes: {stderr.decode()}")
return [], False
except Exception as e:
logging.error(f"Exception during envelope listing: {e}")
return [], False
async def list_accounts() -> Tuple[List[Dict[str, Any]], bool]:
"""
Retrieve a list of accounts configured in Himalaya.
Returns:
Tuple containing:
- List of account dictionaries
- Success status (True if operation was successful)
"""
try:
process = await asyncio.create_subprocess_shell(
"himalaya account list -o json",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
accounts = json.loads(stdout.decode())
return accounts, True
else:
logging.error(f"Error listing accounts: {stderr.decode()}")
return [], False
except Exception as e:
logging.error(f"Exception during account listing: {e}")
return [], False
async def list_folders() -> Tuple[List[Dict[str, Any]], bool]:
"""
Retrieve a list of folders available in Himalaya.
Returns:
Tuple containing:
- List of folder dictionaries
- Success status (True if operation was successful)
"""
try:
process = await asyncio.create_subprocess_shell(
"himalaya folder list -o json",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
folders = json.loads(stdout.decode())
return folders, True
else:
logging.error(f"Error listing folders: {stderr.decode()}")
return [], False
except Exception as e:
logging.error(f"Exception during folder listing: {e}")
return [], False
async def delete_message(message_id: int) -> bool:
"""
Delete a message by its ID.
Args:
message_id: The ID of the message to delete
Returns:
True if deletion was successful, False otherwise
"""
try:
process = await asyncio.create_subprocess_shell(
f"himalaya message delete {message_id}",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
return process.returncode == 0
except Exception as e:
logging.error(f"Exception during message deletion: {e}")
return False
async def archive_message(message_id: int) -> bool:
"""
Archive a message by its ID.
Args:
message_id: The ID of the message to archive
Returns:
True if archiving was successful, False otherwise
"""
try:
process = await asyncio.create_subprocess_shell(
f"himalaya message archive {message_id}",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
return process.returncode == 0
except Exception as e:
logging.error(f"Exception during message archiving: {e}")
return False
async def get_message_content(message_id: int, format: str = "html") -> Tuple[Optional[str], bool]:
"""
Retrieve the content of a message by its ID.
Args:
message_id: The ID of the message to retrieve
format: The desired format of the message content ("html" or "text")
Returns:
Tuple containing:
- Message content (or None if retrieval failed)
- Success status (True if operation was successful)
"""
try:
cmd = f"himalaya message read {message_id}"
if format == "text":
cmd += " -t"
process = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
content = stdout.decode()
return content, True
else:
logging.error(f"Error retrieving message content: {stderr.decode()}")
return None, False
except Exception as e:
logging.error(f"Exception during message content retrieval: {e}")
return None, False

View File

@@ -59,6 +59,6 @@ def get_access_token(scopes):
f.write(cache.serialize())
access_token = token_response['access_token']
headers = {'Authorization': f'Bearer {access_token}', 'Prefer': 'outlook.body-content-type="text"'}
headers = {'Authorization': f'Bearer {access_token}', 'Prefer': 'outlook.body-content-type="text",IdType="ImmutableId"'}
return access_token, headers

View File

@@ -152,7 +152,7 @@ async def delete_mail_async(maildir_path, headers, progress, task_id, dry_run=Fa
progress.console.print(f"[DRY-RUN] Would delete message: {message_id}")
progress.advance(task_id)
async def synchronize_maildir_async(maildir_path, headers, progress, task_id, dry_run=False):
async def synchronize_maildir_async(maildir_path, headers, progress, task_id, dry_run=False):
"""
Synchronize Maildir with Microsoft Graph API.

View File

@@ -0,0 +1,17 @@
"""
Taskwarrior API module for interacting with the Taskwarrior command-line task manager.
"""
from apis.taskwarrior.client import (
create_task,
list_tasks,
complete_task,
delete_task,
)
__all__ = [
"create_task",
"list_tasks",
"complete_task",
"delete_task",
]

146
apis/taskwarrior/client.py Normal file
View File

@@ -0,0 +1,146 @@
import asyncio
import json
import logging
from typing import Tuple, List, Dict, Any, Optional, Union
async def create_task(task_description: str, tags: List[str] = None, project: str = None,
due: str = None, priority: str = None) -> Tuple[bool, Optional[str]]:
"""
Create a new task using the Taskwarrior CLI.
Args:
task_description: Description of the task
tags: List of tags to apply to the task
project: Project to which the task belongs
due: Due date in the format that Taskwarrior accepts
priority: Priority of the task (H, M, L)
Returns:
Tuple containing:
- Success status (True if operation was successful)
- Task ID or error message
"""
try:
cmd = ["task", "add"]
# Add project if specified
if project:
cmd.append(f"project:{project}")
# Add tags if specified
if tags:
for tag in tags:
cmd.append(f"+{tag}")
# Add due date if specified
if due:
cmd.append(f"due:{due}")
# Add priority if specified
if priority and priority in ["H", "M", "L"]:
cmd.append(f"priority:{priority}")
# Add task description
cmd.append(task_description)
# Convert command list to string
cmd_str = " ".join(cmd)
process = await asyncio.create_subprocess_shell(
cmd_str,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
return True, stdout.decode().strip()
else:
error_msg = stderr.decode().strip()
logging.error(f"Error creating task: {error_msg}")
return False, error_msg
except Exception as e:
logging.error(f"Exception during task creation: {e}")
return False, str(e)
async def list_tasks(filter_str: str = "") -> Tuple[List[Dict[str, Any]], bool]:
"""
List tasks from Taskwarrior.
Args:
filter_str: Optional filter string to pass to Taskwarrior
Returns:
Tuple containing:
- List of task dictionaries
- Success status (True if operation was successful)
"""
try:
cmd = f"task {filter_str} export"
process = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
if process.returncode == 0:
tasks = json.loads(stdout.decode())
return tasks, True
else:
logging.error(f"Error listing tasks: {stderr.decode()}")
return [], False
except Exception as e:
logging.error(f"Exception during task listing: {e}")
return [], False
async def complete_task(task_id: str) -> bool:
"""
Mark a task as completed.
Args:
task_id: ID of the task to complete
Returns:
True if task was completed successfully, False otherwise
"""
try:
cmd = f"echo 'yes' | task {task_id} done"
process = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
return process.returncode == 0
except Exception as e:
logging.error(f"Exception during task completion: {e}")
return False
async def delete_task(task_id: str) -> bool:
"""
Delete a task.
Args:
task_id: ID of the task to delete
Returns:
True if task was deleted successfully, False otherwise
"""
try:
cmd = f"echo 'yes' | task {task_id} delete"
process = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await process.communicate()
return process.returncode == 0
except Exception as e:
logging.error(f"Exception during task deletion: {e}")
return False