add new tasks
This commit is contained in:
@@ -1,3 +1,35 @@
|
||||
"""
|
||||
Mail utilities module for email operations.
|
||||
"""
|
||||
|
||||
from .helpers import (
|
||||
ensure_directory_exists,
|
||||
format_datetime,
|
||||
format_mail_link,
|
||||
format_mail_link_comment,
|
||||
format_mime_date,
|
||||
has_mail_link,
|
||||
load_last_sync_timestamp,
|
||||
parse_mail_link,
|
||||
parse_maildir_name,
|
||||
remove_mail_link_comment,
|
||||
safe_filename,
|
||||
save_sync_timestamp,
|
||||
truncate_id,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"ensure_directory_exists",
|
||||
"format_datetime",
|
||||
"format_mail_link",
|
||||
"format_mail_link_comment",
|
||||
"format_mime_date",
|
||||
"has_mail_link",
|
||||
"load_last_sync_timestamp",
|
||||
"parse_mail_link",
|
||||
"parse_maildir_name",
|
||||
"remove_mail_link_comment",
|
||||
"safe_filename",
|
||||
"save_sync_timestamp",
|
||||
"truncate_id",
|
||||
]
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
"""
|
||||
Mail utility helper functions.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
import email.utils
|
||||
|
||||
|
||||
def truncate_id(message_id, length=8):
|
||||
"""
|
||||
Truncate a message ID to a reasonable length for display.
|
||||
@@ -24,6 +26,7 @@ def truncate_id(message_id, length=8):
|
||||
return message_id
|
||||
return f"{message_id[:length]}..."
|
||||
|
||||
|
||||
def load_last_sync_timestamp():
|
||||
"""
|
||||
Load the last synchronization timestamp from a file.
|
||||
@@ -32,12 +35,13 @@ def load_last_sync_timestamp():
|
||||
float: The timestamp of the last synchronization, or 0 if not available.
|
||||
"""
|
||||
try:
|
||||
with open('sync_timestamp.json', 'r') as f:
|
||||
with open("sync_timestamp.json", "r") as f:
|
||||
data = json.load(f)
|
||||
return data.get('timestamp', 0)
|
||||
return data.get("timestamp", 0)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return 0
|
||||
|
||||
|
||||
def save_sync_timestamp():
|
||||
"""
|
||||
Save the current timestamp as the last synchronization timestamp.
|
||||
@@ -46,8 +50,9 @@ def save_sync_timestamp():
|
||||
None
|
||||
"""
|
||||
current_time = time.time()
|
||||
with open('sync_timestamp.json', 'w') as f:
|
||||
json.dump({'timestamp': current_time}, f)
|
||||
with open("sync_timestamp.json", "w") as f:
|
||||
json.dump({"timestamp": current_time}, f)
|
||||
|
||||
|
||||
def format_datetime(dt_str, format_string="%m/%d %I:%M %p"):
|
||||
"""
|
||||
@@ -63,11 +68,12 @@ def format_datetime(dt_str, format_string="%m/%d %I:%M %p"):
|
||||
if not dt_str:
|
||||
return ""
|
||||
try:
|
||||
dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00'))
|
||||
dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
|
||||
return dt.strftime(format_string)
|
||||
except (ValueError, AttributeError):
|
||||
return dt_str
|
||||
|
||||
|
||||
def format_mime_date(dt_str):
|
||||
"""
|
||||
Format a datetime string from ISO format to RFC 5322 format for MIME Date headers.
|
||||
@@ -81,11 +87,12 @@ def format_mime_date(dt_str):
|
||||
if not dt_str:
|
||||
return ""
|
||||
try:
|
||||
dt = datetime.fromisoformat(dt_str.replace('Z', '+00:00'))
|
||||
dt = datetime.fromisoformat(dt_str.replace("Z", "+00:00"))
|
||||
return email.utils.format_datetime(dt)
|
||||
except (ValueError, AttributeError):
|
||||
return dt_str
|
||||
|
||||
|
||||
def safe_filename(filename):
|
||||
"""
|
||||
Convert a string to a safe filename.
|
||||
@@ -98,9 +105,10 @@ def safe_filename(filename):
|
||||
"""
|
||||
invalid_chars = '<>:"/\\|?*'
|
||||
for char in invalid_chars:
|
||||
filename = filename.replace(char, '_')
|
||||
filename = filename.replace(char, "_")
|
||||
return filename
|
||||
|
||||
|
||||
def ensure_directory_exists(directory):
|
||||
"""
|
||||
Ensure that a directory exists, creating it if necessary.
|
||||
@@ -114,6 +122,7 @@ def ensure_directory_exists(directory):
|
||||
if not os.path.exists(directory):
|
||||
os.makedirs(directory)
|
||||
|
||||
|
||||
def parse_maildir_name(filename):
|
||||
"""
|
||||
Parse a Maildir filename to extract components.
|
||||
@@ -125,9 +134,104 @@ def parse_maildir_name(filename):
|
||||
tuple: (message_id, flags) components of the filename.
|
||||
"""
|
||||
# Maildir filename format: unique-id:flags
|
||||
if ':' in filename:
|
||||
message_id, flags = filename.split(':', 1)
|
||||
if ":" in filename:
|
||||
message_id, flags = filename.split(":", 1)
|
||||
else:
|
||||
message_id = filename
|
||||
flags = ''
|
||||
flags = ""
|
||||
return message_id, flags
|
||||
|
||||
|
||||
# Mail-Task Link Utilities
|
||||
# These functions handle the mail://message-id links that connect tasks to emails
|
||||
|
||||
MAIL_LINK_PREFIX = "mail://"
|
||||
MAIL_LINK_COMMENT_PATTERN = r"<!--\s*mail://([^>\s]+)\s*-->"
|
||||
|
||||
|
||||
def format_mail_link(message_id: str) -> str:
|
||||
"""
|
||||
Format a message ID as a mail link URI.
|
||||
|
||||
Args:
|
||||
message_id: The email message ID (e.g., "abc123@example.com")
|
||||
|
||||
Returns:
|
||||
Mail link URI (e.g., "mail://abc123@example.com")
|
||||
"""
|
||||
# Clean up message ID - remove angle brackets if present
|
||||
message_id = message_id.strip()
|
||||
if message_id.startswith("<"):
|
||||
message_id = message_id[1:]
|
||||
if message_id.endswith(">"):
|
||||
message_id = message_id[:-1]
|
||||
return f"{MAIL_LINK_PREFIX}{message_id}"
|
||||
|
||||
|
||||
def format_mail_link_comment(message_id: str) -> str:
|
||||
"""
|
||||
Format a message ID as an HTML comment for embedding in task notes.
|
||||
|
||||
Args:
|
||||
message_id: The email message ID
|
||||
|
||||
Returns:
|
||||
HTML comment containing the mail link (e.g., "<!-- mail://abc123@example.com -->")
|
||||
"""
|
||||
return f"<!-- {format_mail_link(message_id)} -->"
|
||||
|
||||
|
||||
def parse_mail_link(notes: str) -> str | None:
|
||||
"""
|
||||
Extract a mail link message ID from task notes.
|
||||
|
||||
Looks for an HTML comment in the format: <!-- mail://message-id -->
|
||||
|
||||
Args:
|
||||
notes: The task notes content
|
||||
|
||||
Returns:
|
||||
The message ID if found, None otherwise
|
||||
"""
|
||||
import re
|
||||
|
||||
if not notes:
|
||||
return None
|
||||
|
||||
match = re.search(MAIL_LINK_COMMENT_PATTERN, notes)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def has_mail_link(notes: str) -> bool:
|
||||
"""
|
||||
Check if task notes contain a mail link.
|
||||
|
||||
Args:
|
||||
notes: The task notes content
|
||||
|
||||
Returns:
|
||||
True if a mail link is found, False otherwise
|
||||
"""
|
||||
return parse_mail_link(notes) is not None
|
||||
|
||||
|
||||
def remove_mail_link_comment(notes: str) -> str:
|
||||
"""
|
||||
Remove the mail link comment from task notes.
|
||||
|
||||
Args:
|
||||
notes: The task notes content
|
||||
|
||||
Returns:
|
||||
Notes with the mail link comment removed
|
||||
"""
|
||||
import re
|
||||
|
||||
if not notes:
|
||||
return ""
|
||||
|
||||
# Remove the mail link comment and any trailing newlines
|
||||
cleaned = re.sub(MAIL_LINK_COMMENT_PATTERN + r"\n*", "", notes)
|
||||
return cleaned.strip()
|
||||
|
||||
Reference in New Issue
Block a user