trying a simple shell script and fixing archives
This commit is contained in:
202
fetch_outlook.py
202
fetch_outlook.py
@@ -1,202 +0,0 @@
|
|||||||
"""
|
|
||||||
Fetch and synchronize emails and calendar events from Microsoft Outlook (Graph API).
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
import argparse
|
|
||||||
import asyncio
|
|
||||||
from rich.panel import Panel
|
|
||||||
from rich.progress import Progress, SpinnerColumn, MofNCompleteColumn
|
|
||||||
|
|
||||||
# Import the refactored modules
|
|
||||||
from apis.microsoft_graph.auth import get_access_token
|
|
||||||
from apis.microsoft_graph.mail import fetch_mail_async, archive_mail_async, delete_mail_async, synchronize_maildir_async
|
|
||||||
from apis.microsoft_graph.calendar import fetch_calendar_events
|
|
||||||
from utils.calendar_utils import save_events_to_vdir, save_events_to_file
|
|
||||||
from utils.mail_utils.helpers import ensure_directory_exists
|
|
||||||
|
|
||||||
# Add argument parsing for dry-run mode
|
|
||||||
arg_parser = argparse.ArgumentParser(description="Fetch and synchronize emails.")
|
|
||||||
arg_parser.add_argument("--dry-run", action="store_true", help="Run in dry-run mode without making changes.", default=False)
|
|
||||||
arg_parser.add_argument("--vdir", help="Output calendar events in vdir format to the specified directory (each event in its own file)", default=None)
|
|
||||||
arg_parser.add_argument("--icsfile", help="Output calendar events into this ics file path.", default=None)
|
|
||||||
arg_parser.add_argument("--org", help="Specify the organization name for the subfolder to store emails and calendar events", default="corteva")
|
|
||||||
arg_parser.add_argument("--days-back", type=int, help="Number of days to look back for calendar events", default=1)
|
|
||||||
arg_parser.add_argument("--days-forward", type=int, help="Number of days to look forward for calendar events", default=6)
|
|
||||||
arg_parser.add_argument("--continue-iteration", action="store_true", help="Enable interactive mode to continue fetching more date ranges", default=False)
|
|
||||||
arg_parser.add_argument("--download-attachments", action="store_true", help="Download email attachments", default=False)
|
|
||||||
args = arg_parser.parse_args()
|
|
||||||
|
|
||||||
# Parse command line arguments
|
|
||||||
dry_run = args.dry_run
|
|
||||||
vdir_path = args.vdir
|
|
||||||
ics_path = args.icsfile
|
|
||||||
org_name = args.org
|
|
||||||
days_back = args.days_back
|
|
||||||
days_forward = args.days_forward
|
|
||||||
continue_iteration = args.continue_iteration
|
|
||||||
download_attachments = args.download_attachments
|
|
||||||
|
|
||||||
async def fetch_calendar_async(headers, progress, task_id):
|
|
||||||
"""
|
|
||||||
Fetch calendar events and save them in the appropriate format.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
headers: Authentication headers for Microsoft Graph API
|
|
||||||
progress: Progress instance for updating progress bars
|
|
||||||
task_id: ID of the task in the progress bar
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of event dictionaries
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: If there's an error fetching or saving events
|
|
||||||
"""
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Use the utility function to fetch calendar events
|
|
||||||
progress.console.print("[cyan]Fetching events from Microsoft Graph API...[/cyan]")
|
|
||||||
events, total_events = await fetch_calendar_events(
|
|
||||||
headers=headers,
|
|
||||||
days_back=days_back,
|
|
||||||
days_forward=days_forward
|
|
||||||
)
|
|
||||||
|
|
||||||
progress.console.print(f"[cyan]Got {len(events)} events from API (reported total: {total_events})[/cyan]")
|
|
||||||
|
|
||||||
# Update progress bar with total events
|
|
||||||
progress.update(task_id, total=total_events)
|
|
||||||
|
|
||||||
# Save events to appropriate format
|
|
||||||
if not dry_run:
|
|
||||||
if vdir_path:
|
|
||||||
# Create org-specific directory within vdir path
|
|
||||||
org_vdir_path = os.path.join(vdir_path, org_name)
|
|
||||||
progress.console.print(f"[cyan]Saving events to vdir: {org_vdir_path}[/cyan]")
|
|
||||||
save_events_to_vdir(events, org_vdir_path, progress, task_id, dry_run)
|
|
||||||
progress.console.print(f"[green]Finished saving events to vdir: {org_vdir_path}[/green]")
|
|
||||||
elif ics_path:
|
|
||||||
# Save to a single ICS file in the output_ics directory
|
|
||||||
progress.console.print(f"[cyan]Saving events to ICS file: {ics_path}/events_latest.ics[/cyan]")
|
|
||||||
save_events_to_file(events, f"{ics_path}/events_latest.ics", progress, task_id, dry_run)
|
|
||||||
progress.console.print(f"[green]Finished saving events to ICS file[/green]")
|
|
||||||
else:
|
|
||||||
# No destination specified
|
|
||||||
progress.console.print("[yellow]Warning: No destination path (--vdir or --icsfile) specified for calendar events.[/yellow]")
|
|
||||||
else:
|
|
||||||
progress.console.print(f"[DRY-RUN] Would save {len(events)} events to {'vdir format' if vdir_path else 'single ICS file'}")
|
|
||||||
progress.update(task_id, advance=len(events))
|
|
||||||
|
|
||||||
# Interactive mode: Ask if the user wants to continue with the next date range
|
|
||||||
if continue_iteration:
|
|
||||||
# Move to the next date range
|
|
||||||
next_start_date = datetime.now() - timedelta(days=days_back)
|
|
||||||
next_end_date = next_start_date + timedelta(days=days_forward)
|
|
||||||
|
|
||||||
progress.console.print(f"\nCurrent date range: {next_start_date.strftime('%Y-%m-%d')} to {next_end_date.strftime('%Y-%m-%d')}")
|
|
||||||
|
|
||||||
user_response = input("\nContinue to iterate? [y/N]: ").strip().lower()
|
|
||||||
|
|
||||||
while user_response == 'y':
|
|
||||||
progress.console.print(f"\nFetching events for {next_start_date.strftime('%Y-%m-%d')} to {next_end_date.strftime('%Y-%m-%d')}...")
|
|
||||||
|
|
||||||
# Reset the progress bar for the new fetch
|
|
||||||
progress.update(task_id, completed=0, total=0)
|
|
||||||
|
|
||||||
# Fetch events for the next date range
|
|
||||||
next_events, next_total_events = await fetch_calendar_events(
|
|
||||||
headers=headers,
|
|
||||||
days_back=0,
|
|
||||||
days_forward=days_forward,
|
|
||||||
start_date=next_start_date,
|
|
||||||
end_date=next_end_date
|
|
||||||
)
|
|
||||||
|
|
||||||
# Update progress bar with total events
|
|
||||||
progress.update(task_id, total=next_total_events)
|
|
||||||
|
|
||||||
if not dry_run:
|
|
||||||
if vdir_path:
|
|
||||||
save_events_to_vdir(next_events, org_vdir_path, progress, task_id, dry_run)
|
|
||||||
else:
|
|
||||||
save_events_to_file(next_events, f'output_ics/outlook_events_{next_start_date.strftime("%Y%m%d")}.ics',
|
|
||||||
progress, task_id, dry_run)
|
|
||||||
else:
|
|
||||||
progress.console.print(f"[DRY-RUN] Would save {len(next_events)} events to {'vdir format' if vdir_path else 'output_ics/outlook_events_' + next_start_date.strftime("%Y%m%d") + '.ics'}")
|
|
||||||
progress.update(task_id, advance=len(next_events))
|
|
||||||
|
|
||||||
# Calculate the next date range
|
|
||||||
next_start_date = next_end_date
|
|
||||||
next_end_date = next_start_date + timedelta(days=days_forward)
|
|
||||||
|
|
||||||
progress.console.print(f"\nNext date range would be: {next_start_date.strftime('%Y-%m-%d')} to {next_end_date.strftime('%Y-%m-%d')}")
|
|
||||||
user_response = input("\nContinue to iterate? [y/N]: ").strip().lower()
|
|
||||||
|
|
||||||
return events
|
|
||||||
except Exception as e:
|
|
||||||
progress.console.print(f"[red]Error fetching or saving calendar events: {str(e)}[/red]")
|
|
||||||
import traceback
|
|
||||||
progress.console.print(f"[red]{traceback.format_exc()}[/red]")
|
|
||||||
progress.update(task_id, completed=True)
|
|
||||||
return []
|
|
||||||
|
|
||||||
# Function to create Maildir structure
|
|
||||||
def create_maildir_structure(base_path):
|
|
||||||
"""
|
|
||||||
Create the standard Maildir directory structure.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
base_path (str): Base path for the Maildir.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
ensure_directory_exists(os.path.join(base_path, 'cur'))
|
|
||||||
ensure_directory_exists(os.path.join(base_path, 'new'))
|
|
||||||
ensure_directory_exists(os.path.join(base_path, 'tmp'))
|
|
||||||
ensure_directory_exists(os.path.join(base_path, '.Archives'))
|
|
||||||
ensure_directory_exists(os.path.join(base_path, '.Trash', 'cur'))
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""
|
|
||||||
Main function to run the script.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
None
|
|
||||||
"""
|
|
||||||
# Save emails to Maildir
|
|
||||||
maildir_path = os.getenv('MAILDIR_PATH', os.path.expanduser('~/Mail')) + f"/{org_name}"
|
|
||||||
attachments_dir = os.path.join(maildir_path, 'attachments')
|
|
||||||
ensure_directory_exists(attachments_dir)
|
|
||||||
create_maildir_structure(maildir_path)
|
|
||||||
|
|
||||||
# Define scopes for Microsoft Graph API
|
|
||||||
scopes = ['https://graph.microsoft.com/Calendars.Read', 'https://graph.microsoft.com/Mail.ReadWrite']
|
|
||||||
|
|
||||||
# Authenticate and get access token
|
|
||||||
access_token, headers = get_access_token(scopes)
|
|
||||||
|
|
||||||
# Set up the progress bars
|
|
||||||
progress = Progress(
|
|
||||||
SpinnerColumn(),
|
|
||||||
MofNCompleteColumn(),
|
|
||||||
*Progress.get_default_columns()
|
|
||||||
)
|
|
||||||
|
|
||||||
with progress:
|
|
||||||
task_fetch = progress.add_task("[green]Syncing Inbox...", total=0)
|
|
||||||
task_calendar = progress.add_task("[cyan]Fetching calendar...", total=0)
|
|
||||||
task_read = progress.add_task("[blue]Marking as read...", total=0)
|
|
||||||
task_archive = progress.add_task("[yellow]Archiving mail...", total=0)
|
|
||||||
task_delete = progress.add_task("[red]Deleting mail...", total=0)
|
|
||||||
|
|
||||||
await asyncio.gather(
|
|
||||||
synchronize_maildir_async(maildir_path, headers, progress, task_read, dry_run),
|
|
||||||
archive_mail_async(maildir_path, headers, progress, task_archive, dry_run),
|
|
||||||
delete_mail_async(maildir_path, headers, progress, task_delete, dry_run),
|
|
||||||
fetch_mail_async(maildir_path, attachments_dir, headers, progress, task_fetch, dry_run, download_attachments),
|
|
||||||
fetch_calendar_async(headers, progress, task_calendar)
|
|
||||||
)
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
asyncio.run(main())
|
|
||||||
140
run_himalaya.sh
140
run_himalaya.sh
@@ -1,140 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Check if an argument is provided
|
|
||||||
if [ -z "$1" ]; then
|
|
||||||
echo "Usage: $0 <message_number>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Function to refresh the vim-himalaya buffer
|
|
||||||
refresh_vim_himalaya() {
|
|
||||||
nvim --server /tmp/nvim-server --remote-send "Himalaya<CR>"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to read a single character without waiting for Enter
|
|
||||||
read_char() {
|
|
||||||
stty -echo -icanon time 0 min 1
|
|
||||||
char=$(dd bs=1 count=1 2>/dev/null)
|
|
||||||
stty echo icanon
|
|
||||||
echo "$char"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to safely run the himalaya command and handle failures
|
|
||||||
run_himalaya_message_read() {
|
|
||||||
himalaya message read "$1" | glow -p
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Failed to open message $1."
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# Function to prompt the user for an action
|
|
||||||
prompt_action() {
|
|
||||||
echo "What would you like to do?"
|
|
||||||
|
|
||||||
# Step 1: Ask if the user wants to create a task
|
|
||||||
echo -n "Would you like to create a task for this message? (y/n): "
|
|
||||||
create_task=$(read_char)
|
|
||||||
echo "$create_task" # Echo the character for feedback
|
|
||||||
if [[ "$create_task" == "y" || "$create_task" == "Y" ]]; then
|
|
||||||
read -p "Task title: " task_title
|
|
||||||
task add "Followup on email $1 - $task_title" --project "Email" --due "$(date -d '+1 week' +%Y-%m-%d)" --priority "P3" --tags "email"
|
|
||||||
echo "Task created for message $1."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Step 2: Ask if the user wants to delete or archive the message
|
|
||||||
echo "d) Delete the message"
|
|
||||||
echo "a) Move the message to the archive folder"
|
|
||||||
echo "x) Skip delete/archive step"
|
|
||||||
echo -n "Enter your choice (d/a/x): "
|
|
||||||
archive_or_delete=$(read_char)
|
|
||||||
echo "$archive_or_delete" # Echo the character for feedback
|
|
||||||
|
|
||||||
case $archive_or_delete in
|
|
||||||
d)
|
|
||||||
echo "Deleting message $1..."
|
|
||||||
himalaya message delete "$1"
|
|
||||||
refresh_vim_himalaya
|
|
||||||
;;
|
|
||||||
a)
|
|
||||||
echo "Archiving message $1..."
|
|
||||||
himalaya message move Archives "$1"
|
|
||||||
refresh_vim_himalaya
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Invalid choice. Skipping delete/archive step."
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
# Step 3: Ask if the user wants to open the next message or exit
|
|
||||||
echo -e "\n"
|
|
||||||
echo "n) Open the next message"
|
|
||||||
echo "p) Open the previous message"
|
|
||||||
echo "x) Exit"
|
|
||||||
echo -n "Enter your choice (o/x): "
|
|
||||||
next_or_exit=$(read_char)
|
|
||||||
echo "$next_or_exit" # Echo the character for feedback
|
|
||||||
|
|
||||||
case $next_or_exit in
|
|
||||||
n)
|
|
||||||
# Try opening the next message, retrying up to 5 times if necessary
|
|
||||||
attempts=0
|
|
||||||
success=false
|
|
||||||
while [ $attempts -lt 5 ]; do
|
|
||||||
next_id=$(( $1 + attempts + 1 ))
|
|
||||||
echo "Attempting to open next message: $next_id"
|
|
||||||
if run_himalaya_message_read "$next_id"; then
|
|
||||||
success=true
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo "Failed to open message $next_id. Retrying..."
|
|
||||||
attempts=$((attempts + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$success" = false ]; then
|
|
||||||
echo "Unable to open any messages after 5 attempts. Exiting."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
p)
|
|
||||||
# Try opening the previous message, retrying up to 5 times if necessary
|
|
||||||
attempts=0
|
|
||||||
success=false
|
|
||||||
while [ $attempts -lt 5 ]; do
|
|
||||||
prev_id=$(( $1 - attempts - 1 ))
|
|
||||||
echo "Attempting to open previous message: $prev_id"
|
|
||||||
if $0 $prev_id; then
|
|
||||||
success=true
|
|
||||||
break
|
|
||||||
else
|
|
||||||
echo "Failed to open message $prev_id. Retrying..."
|
|
||||||
attempts=$((attempts + 1))
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "$success" = false ]; then
|
|
||||||
echo "Unable to open any messages after 5 attempts. Exiting."
|
|
||||||
fi
|
|
||||||
;;
|
|
||||||
x)
|
|
||||||
echo "Exiting."
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Invalid choice. Exiting."
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
}
|
|
||||||
|
|
||||||
# Run the himalaya command with the provided message number
|
|
||||||
run_himalaya_message_read "$1"
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "Error reading message $1. Exiting."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Prompt the user for the next action
|
|
||||||
prompt_action "$1"
|
|
||||||
38
shell/email_processor.awk
Executable file
38
shell/email_processor.awk
Executable file
@@ -0,0 +1,38 @@
|
|||||||
|
#!/usr/bin/awk -f
|
||||||
|
|
||||||
|
# Primary email processor using AWK
|
||||||
|
# Lightweight, portable text processing for cleaning up email content
|
||||||
|
|
||||||
|
{
|
||||||
|
# Remove URL defense wrappers step by step
|
||||||
|
gsub(/https:\/\/urldefense\.com\/v3\/__/, "")
|
||||||
|
gsub(/__[^[:space:]]*/, "")
|
||||||
|
|
||||||
|
# Extract and shorten URLs to domains
|
||||||
|
# This processes all URLs in the line
|
||||||
|
while (match($0, /https?:\/\/[^\/[:space:]]+/)) {
|
||||||
|
url = substr($0, RSTART, RLENGTH)
|
||||||
|
# Extract domain (remove protocol)
|
||||||
|
domain = url
|
||||||
|
gsub(/^https?:\/\//, "", domain)
|
||||||
|
# Replace this URL with [domain]
|
||||||
|
sub(url, "[" domain "]", $0)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove any remaining URL paths after domain extraction
|
||||||
|
gsub(/\][^[:space:]]*/, "]")
|
||||||
|
|
||||||
|
# Remove mailto links
|
||||||
|
gsub(/mailto:[^[:space:]]*/, "")
|
||||||
|
|
||||||
|
# Clean up email headers - make them bold
|
||||||
|
if (/^From:/) { gsub(/^From:[[:space:]]*/, "**From:** ") }
|
||||||
|
if (/^To:/) { gsub(/^To:[[:space:]]*/, "**To:** ") }
|
||||||
|
if (/^Subject:/) { gsub(/^Subject:[[:space:]]*/, "**Subject:** ") }
|
||||||
|
if (/^Date:/) { gsub(/^Date:[[:space:]]*/, "**Date:** ") }
|
||||||
|
|
||||||
|
# Skip empty lines
|
||||||
|
if (/^[[:space:]]*$/) next
|
||||||
|
|
||||||
|
print
|
||||||
|
}
|
||||||
125
shell/email_processor.py
Executable file
125
shell/email_processor.py
Executable file
@@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Email content processor for run_himalaya.sh
|
||||||
|
Cleans up email content for better readability
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import re
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
|
||||||
|
def extract_domain(url):
|
||||||
|
"""Extract domain from URL"""
|
||||||
|
try:
|
||||||
|
parsed = urlparse(url if url.startswith("http") else "http://" + url)
|
||||||
|
return parsed.netloc.replace("www.", "")
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_important_url(url, domain):
|
||||||
|
"""Determine if URL should be shown in full"""
|
||||||
|
important_domains = [
|
||||||
|
"github.com",
|
||||||
|
"gitlab.com",
|
||||||
|
"jira.",
|
||||||
|
"confluence.",
|
||||||
|
"docs.google.com",
|
||||||
|
"drive.google.com",
|
||||||
|
"sharepoint.com",
|
||||||
|
"slack.com",
|
||||||
|
"teams.microsoft.com",
|
||||||
|
"zoom.us",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Show short URLs in full
|
||||||
|
if len(url) < 60:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Show URLs with important domains in shortened form
|
||||||
|
if domain and any(imp in domain for imp in important_domains):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def clean_url_defenses(text):
|
||||||
|
"""Remove URL defense wrappers"""
|
||||||
|
# Remove Proofpoint URL defense
|
||||||
|
text = re.sub(r"https://urldefense\.com/v3/__([^;]+)__;[^$]*\$", r"\1", text)
|
||||||
|
|
||||||
|
# Remove other common URL wrappers
|
||||||
|
text = re.sub(
|
||||||
|
r"https://[^/]*phishalarm[^/]*/[^/]*/[^$]*\$", "[Security Link Removed]", text
|
||||||
|
)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def process_email_content(content):
|
||||||
|
"""Process email content for better readability"""
|
||||||
|
|
||||||
|
# Remove URL defense wrappers first
|
||||||
|
content = clean_url_defenses(content)
|
||||||
|
|
||||||
|
# Clean up common email artifacts first
|
||||||
|
content = re.sub(
|
||||||
|
r"ZjQcmQRYFpfpt.*?ZjQcmQRYFpfpt\w+End",
|
||||||
|
"[Security Banner Removed]",
|
||||||
|
content,
|
||||||
|
flags=re.DOTALL,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up mailto links that clutter the display
|
||||||
|
content = re.sub(r"mailto:[^\s>]+", "", content)
|
||||||
|
|
||||||
|
# Pattern to match URLs (more conservative)
|
||||||
|
url_pattern = r'https?://[^\s<>"{}|\\^`\[\]\(\)]+[^\s<>"{}|\\^`\[\]\(\).,;:!?]'
|
||||||
|
|
||||||
|
def replace_url(match):
|
||||||
|
url = match.group(0)
|
||||||
|
domain = extract_domain(url)
|
||||||
|
|
||||||
|
if is_important_url(url, domain):
|
||||||
|
if domain and len(url) > 60:
|
||||||
|
return f"[{domain}]"
|
||||||
|
else:
|
||||||
|
return url
|
||||||
|
else:
|
||||||
|
if domain:
|
||||||
|
return f"[{domain}]"
|
||||||
|
else:
|
||||||
|
return "[Link]"
|
||||||
|
|
||||||
|
# Replace URLs
|
||||||
|
content = re.sub(url_pattern, replace_url, content)
|
||||||
|
|
||||||
|
# Clean up email headers formatting
|
||||||
|
content = re.sub(
|
||||||
|
r"^(From|To|Subject|Date):\s*(.+?)$", r"**\1:** \2", content, flags=re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Clean up angle brackets around email addresses that are left over
|
||||||
|
content = re.sub(r"<[^>]*@[^>]*>", "", content)
|
||||||
|
|
||||||
|
# Remove excessive whitespace but preserve paragraph breaks
|
||||||
|
content = re.sub(r"\n\s*\n\s*\n+", "\n\n", content)
|
||||||
|
content = re.sub(r"[ \t]+", " ", content)
|
||||||
|
|
||||||
|
# Remove lines that are just whitespace
|
||||||
|
content = re.sub(r"^\s*$\n", "", content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
# Clean up repeated domain references on same line
|
||||||
|
content = re.sub(r"\[([^\]]+)\].*?\[\1\]", r"[\1]", content)
|
||||||
|
|
||||||
|
# Clean up trailing angle brackets and other artifacts
|
||||||
|
content = re.sub(r"[<>]+\s*$", "", content, flags=re.MULTILINE)
|
||||||
|
|
||||||
|
return content.strip()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
content = sys.stdin.read()
|
||||||
|
processed = process_email_content(content)
|
||||||
|
print(processed)
|
||||||
262
shell/run_himalaya.sh
Executable file
262
shell/run_himalaya.sh
Executable file
@@ -0,0 +1,262 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Enhanced Himalaya Email Management Script
|
||||||
|
# Features: Auto-discovery, smart navigation, streamlined UX
|
||||||
|
|
||||||
|
# Function to get available message IDs from himalaya
|
||||||
|
get_available_message_ids() {
|
||||||
|
himalaya envelope list | awk 'NR > 2 && /^\| [0-9]/ {gsub(/[| ]/, "", $2); if($2 ~ /^[0-9]+$/) print $2}' | sort -n
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get the latest (most recent) message ID
|
||||||
|
get_latest_message_id() {
|
||||||
|
get_available_message_ids | tail -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to find the next valid message ID
|
||||||
|
find_next_message_id() {
|
||||||
|
local current_id="$1"
|
||||||
|
get_available_message_ids | awk -v current="$current_id" '$1 > current {print $1; exit}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to find the previous valid message ID
|
||||||
|
find_previous_message_id() {
|
||||||
|
local current_id="$1"
|
||||||
|
get_available_message_ids | awk -v current="$current_id" '$1 < current {prev=$1} END {if(prev) print prev}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to refresh the vim-himalaya buffer
|
||||||
|
refresh_vim_himalaya() {
|
||||||
|
if [ -S "/tmp/nvim-server" ]; then
|
||||||
|
nvim --server /tmp/nvim-server --remote-send "Himalaya<CR>" 2>/dev/null
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to read a single character without waiting for Enter
|
||||||
|
read_char() {
|
||||||
|
stty -echo -icanon time 0 min 1
|
||||||
|
char=$(dd bs=1 count=1 2>/dev/null)
|
||||||
|
stty echo icanon
|
||||||
|
echo "$char"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to safely run the himalaya command and handle failures
|
||||||
|
run_himalaya_message_read() {
|
||||||
|
local message_id="$1"
|
||||||
|
local temp_output
|
||||||
|
temp_output=$(himalaya message read "$message_id" 2>&1)
|
||||||
|
local exit_code=$?
|
||||||
|
|
||||||
|
if [ $exit_code -eq 0 ]; then
|
||||||
|
# Choose email processor based on availability
|
||||||
|
local email_processor
|
||||||
|
if [ -f "email_processor.awk" ]; then
|
||||||
|
email_processor="awk -f email_processor.awk"
|
||||||
|
elif command -v python3 >/dev/null 2>&1 && [ -f "email_processor.py" ]; then
|
||||||
|
email_processor="python3 email_processor.py"
|
||||||
|
else
|
||||||
|
email_processor="cat" # fallback to no processing
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Process email content, then render with glow and display with less
|
||||||
|
echo "$temp_output" | \
|
||||||
|
$email_processor | \
|
||||||
|
# bat
|
||||||
|
glow --width 100 -p
|
||||||
|
# less -R -X -F
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "Failed to open message $message_id."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to create a task for the current message
|
||||||
|
create_task_for_message() {
|
||||||
|
local message_id="$1"
|
||||||
|
read -p "Task title: " task_title
|
||||||
|
if [ -n "$task_title" ]; then
|
||||||
|
if command -v task >/dev/null 2>&1; then
|
||||||
|
task add "Followup on email $message_id - $task_title" \
|
||||||
|
--project "Email" \
|
||||||
|
--due "$(date -d '+1 week' +%Y-%m-%d)" \
|
||||||
|
--priority "P3" \
|
||||||
|
--tags "email"
|
||||||
|
echo "Task created for message $message_id."
|
||||||
|
else
|
||||||
|
echo "TaskWarrior not found. Task not created."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "No task title provided. Task not created."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to display the action menu and handle user choice
|
||||||
|
show_action_menu() {
|
||||||
|
local current_id="$1"
|
||||||
|
|
||||||
|
# Draw a nice border above the menu
|
||||||
|
echo ""
|
||||||
|
echo "════════════════════════════════════════════════════════════════════════════════"
|
||||||
|
echo " ACTIONS"
|
||||||
|
echo "════════════════════════════════════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
|
echo " t) Create task for this message"
|
||||||
|
echo " d) Delete message"
|
||||||
|
echo " a) Archive message"
|
||||||
|
echo " n) Next message"
|
||||||
|
echo " p) Previous message"
|
||||||
|
echo " r) Reopen/redisplay current message"
|
||||||
|
echo " q) Quit"
|
||||||
|
echo ""
|
||||||
|
echo "────────────────────────────────────────────────────────────────────────────────"
|
||||||
|
echo -n "Enter choice: "
|
||||||
|
|
||||||
|
local choice=$(read_char)
|
||||||
|
echo "$choice" # Echo for feedback
|
||||||
|
|
||||||
|
# Clear screen after choice is made
|
||||||
|
clear
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
t|T)
|
||||||
|
create_task_for_message "$current_id"
|
||||||
|
echo "Press any key to continue..."
|
||||||
|
read_char > /dev/null
|
||||||
|
process_message "$current_id"
|
||||||
|
;;
|
||||||
|
d|D)
|
||||||
|
echo "Deleting message $current_id..."
|
||||||
|
if himalaya message delete "$current_id"; then
|
||||||
|
echo "Message $current_id deleted successfully."
|
||||||
|
refresh_vim_himalaya
|
||||||
|
# Try to open next available message
|
||||||
|
local next_id=$(find_next_message_id "$current_id")
|
||||||
|
if [ -n "$next_id" ]; then
|
||||||
|
echo "Opening next message: $next_id"
|
||||||
|
process_message "$next_id"
|
||||||
|
else
|
||||||
|
echo "No more messages. Exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Failed to delete message $current_id."
|
||||||
|
echo "Press any key to continue..."
|
||||||
|
read_char > /dev/null
|
||||||
|
process_message "$current_id"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
a|A)
|
||||||
|
echo "Archiving message $current_id..."
|
||||||
|
if himalaya message move Archives "$current_id"; then
|
||||||
|
echo "Message $current_id archived successfully."
|
||||||
|
refresh_vim_himalaya
|
||||||
|
# Try to open next available message
|
||||||
|
local next_id=$(find_next_message_id "$current_id")
|
||||||
|
if [ -n "$next_id" ]; then
|
||||||
|
echo "Opening next message: $next_id"
|
||||||
|
process_message "$next_id"
|
||||||
|
else
|
||||||
|
echo "No more messages. Exiting."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Failed to archive message $current_id."
|
||||||
|
echo "Press any key to continue..."
|
||||||
|
read_char > /dev/null
|
||||||
|
process_message "$current_id"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
n|N)
|
||||||
|
local next_id=$(find_next_message_id "$current_id")
|
||||||
|
if [ -n "$next_id" ]; then
|
||||||
|
echo "Opening next message: $next_id"
|
||||||
|
process_message "$next_id"
|
||||||
|
else
|
||||||
|
echo "No next message available."
|
||||||
|
echo "Press any key to continue..."
|
||||||
|
read_char > /dev/null
|
||||||
|
process_message "$current_id"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
p|P)
|
||||||
|
local prev_id=$(find_previous_message_id "$current_id")
|
||||||
|
if [ -n "$prev_id" ]; then
|
||||||
|
echo "Opening previous message: $prev_id"
|
||||||
|
process_message "$prev_id"
|
||||||
|
else
|
||||||
|
echo "No previous message available."
|
||||||
|
echo "Press any key to continue..."
|
||||||
|
read_char > /dev/null
|
||||||
|
process_message "$current_id"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
r|R)
|
||||||
|
echo "Reopening message $current_id..."
|
||||||
|
process_message "$current_id"
|
||||||
|
;;
|
||||||
|
q|Q)
|
||||||
|
echo "Exiting."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid choice. Please try again."
|
||||||
|
echo "Press any key to continue..."
|
||||||
|
read_char > /dev/null
|
||||||
|
process_message "$current_id"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to process a message (read and show menu)
|
||||||
|
process_message() {
|
||||||
|
local message_id="$1"
|
||||||
|
local is_fallback="$2"
|
||||||
|
|
||||||
|
echo "Reading message $message_id..."
|
||||||
|
if run_himalaya_message_read "$message_id"; then
|
||||||
|
show_action_menu "$message_id"
|
||||||
|
else
|
||||||
|
echo "Error reading message $message_id."
|
||||||
|
if [ "$is_fallback" = "true" ]; then
|
||||||
|
# If this was already a fallback attempt, don't try again
|
||||||
|
echo "Unable to read any messages. Exiting."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
# Try to find a valid message to fall back to
|
||||||
|
echo "Trying to find an available message..."
|
||||||
|
local latest_id=$(get_latest_message_id)
|
||||||
|
if [ -n "$latest_id" ] && [ "$latest_id" != "$message_id" ]; then
|
||||||
|
echo "Opening latest message: $latest_id"
|
||||||
|
process_message "$latest_id" "true"
|
||||||
|
else
|
||||||
|
echo "No valid messages found. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main script logic
|
||||||
|
|
||||||
|
# Check if message ID is provided, otherwise get the latest
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
echo "No message ID provided. Finding latest message..."
|
||||||
|
message_id=$(get_latest_message_id)
|
||||||
|
if [ -z "$message_id" ]; then
|
||||||
|
echo "No messages found in inbox."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Latest message ID: $message_id"
|
||||||
|
else
|
||||||
|
message_id="$1"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate that himalaya is available
|
||||||
|
if ! command -v himalaya >/dev/null 2>&1; then
|
||||||
|
echo "Error: himalaya command not found. Please install himalaya."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Start processing the message
|
||||||
|
process_message "$message_id"
|
||||||
41
shell/run_tests.sh
Executable file
41
shell/run_tests.sh
Executable file
@@ -0,0 +1,41 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test runner for run_himalaya.sh
|
||||||
|
|
||||||
|
echo "===================="
|
||||||
|
echo "Himalaya Script Test Suite"
|
||||||
|
echo "===================="
|
||||||
|
echo
|
||||||
|
|
||||||
|
# Check if glow is available (needed for message display)
|
||||||
|
if ! command -v glow >/dev/null 2>&1; then
|
||||||
|
echo "Warning: glow not found. Some tests may behave differently."
|
||||||
|
echo "Install with: brew install glow"
|
||||||
|
echo
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get the script directory
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
# Set up PATH to include our test mocks
|
||||||
|
export PATH="$SCRIPT_DIR/tests:$PATH"
|
||||||
|
|
||||||
|
# Run unit tests
|
||||||
|
echo "=== Unit Tests ==="
|
||||||
|
if ! bash "$SCRIPT_DIR/tests/unit_tests.sh"; then
|
||||||
|
echo "Unit tests failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "=== Integration Tests ==="
|
||||||
|
if ! bash "$SCRIPT_DIR/tests/integration_tests.sh"; then
|
||||||
|
echo "Integration tests failed!"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "===================="
|
||||||
|
echo "All tests passed! ✅"
|
||||||
|
echo "===================="
|
||||||
224
src/cli/sync.py
224
src/cli/sync.py
@@ -16,6 +16,7 @@ from src.services.microsoft_graph.mail import (
|
|||||||
)
|
)
|
||||||
from src.services.microsoft_graph.auth import get_access_token
|
from src.services.microsoft_graph.auth import get_access_token
|
||||||
|
|
||||||
|
|
||||||
# Function to create Maildir structure
|
# Function to create Maildir structure
|
||||||
def create_maildir_structure(base_path):
|
def create_maildir_structure(base_path):
|
||||||
"""
|
"""
|
||||||
@@ -34,7 +35,18 @@ def create_maildir_structure(base_path):
|
|||||||
ensure_directory_exists(os.path.join(base_path, ".Trash", "cur"))
|
ensure_directory_exists(os.path.join(base_path, ".Trash", "cur"))
|
||||||
|
|
||||||
|
|
||||||
async def fetch_calendar_async(headers, progress, task_id, dry_run, vdir_path, ics_path, org_name, days_back, days_forward, continue_iteration):
|
async def fetch_calendar_async(
|
||||||
|
headers,
|
||||||
|
progress,
|
||||||
|
task_id,
|
||||||
|
dry_run,
|
||||||
|
vdir_path,
|
||||||
|
ics_path,
|
||||||
|
org_name,
|
||||||
|
days_back,
|
||||||
|
days_forward,
|
||||||
|
continue_iteration,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Fetch calendar events and save them in the appropriate format.
|
Fetch calendar events and save them in the appropriate format.
|
||||||
|
|
||||||
@@ -73,8 +85,7 @@ async def fetch_calendar_async(headers, progress, task_id, dry_run, vdir_path, i
|
|||||||
progress.console.print(
|
progress.console.print(
|
||||||
f"[cyan]Saving events to vdir: {org_vdir_path}[/cyan]"
|
f"[cyan]Saving events to vdir: {org_vdir_path}[/cyan]"
|
||||||
)
|
)
|
||||||
save_events_to_vdir(events, org_vdir_path,
|
save_events_to_vdir(events, org_vdir_path, progress, task_id, dry_run)
|
||||||
progress, task_id, dry_run)
|
|
||||||
progress.console.print(
|
progress.console.print(
|
||||||
f"[green]Finished saving events to vdir: {org_vdir_path}[/green]"
|
f"[green]Finished saving events to vdir: {org_vdir_path}[/green]"
|
||||||
)
|
)
|
||||||
@@ -97,7 +108,8 @@ async def fetch_calendar_async(headers, progress, task_id, dry_run, vdir_path, i
|
|||||||
else:
|
else:
|
||||||
progress.console.print(
|
progress.console.print(
|
||||||
f"[DRY-RUN] Would save {len(events)} events to {
|
f"[DRY-RUN] Would save {len(events)} events to {
|
||||||
'vdir format' if vdir_path else 'single ICS file'}"
|
'vdir format' if vdir_path else 'single ICS file'
|
||||||
|
}"
|
||||||
)
|
)
|
||||||
progress.update(task_id, advance=len(events))
|
progress.update(task_id, advance=len(events))
|
||||||
|
|
||||||
@@ -108,17 +120,22 @@ async def fetch_calendar_async(headers, progress, task_id, dry_run, vdir_path, i
|
|||||||
next_end_date = next_start_date + timedelta(days=days_forward)
|
next_end_date = next_start_date + timedelta(days=days_forward)
|
||||||
|
|
||||||
progress.console.print(
|
progress.console.print(
|
||||||
f"\nCurrent date range: {next_start_date.strftime(
|
f"\nCurrent date range: {next_start_date.strftime('%Y-%m-%d')} to {
|
||||||
'%Y-%m-%d')} to {next_end_date.strftime('%Y-%m-%d')}"
|
next_end_date.strftime('%Y-%m-%d')
|
||||||
|
}"
|
||||||
)
|
)
|
||||||
|
|
||||||
user_response = click.prompt(
|
user_response = (
|
||||||
"\nContinue to iterate? [y/N]", default="N").strip().lower()
|
click.prompt("\nContinue to iterate? [y/N]", default="N")
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
|
)
|
||||||
|
|
||||||
while user_response == "y":
|
while user_response == "y":
|
||||||
progress.console.print(
|
progress.console.print(
|
||||||
f"\nFetching events for {next_start_date.strftime(
|
f"\nFetching events for {next_start_date.strftime('%Y-%m-%d')} to {
|
||||||
'%Y-%m-%d')} to {next_end_date.strftime('%Y-%m-%d')}..."
|
next_end_date.strftime('%Y-%m-%d')
|
||||||
|
}..."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Reset the progress bar for the new fetch
|
# Reset the progress bar for the new fetch
|
||||||
@@ -152,7 +169,12 @@ async def fetch_calendar_async(headers, progress, task_id, dry_run, vdir_path, i
|
|||||||
else:
|
else:
|
||||||
progress.console.print(
|
progress.console.print(
|
||||||
f"[DRY-RUN] Would save {len(next_events)} events to {
|
f"[DRY-RUN] Would save {len(next_events)} events to {
|
||||||
'vdir format' if vdir_path else 'output_ics/outlook_events_' + next_start_date.strftime('%Y%m%d') + '.ics'}"
|
'vdir format'
|
||||||
|
if vdir_path
|
||||||
|
else 'output_ics/outlook_events_'
|
||||||
|
+ next_start_date.strftime('%Y%m%d')
|
||||||
|
+ '.ics'
|
||||||
|
}"
|
||||||
)
|
)
|
||||||
progress.update(task_id, advance=len(next_events))
|
progress.update(task_id, advance=len(next_events))
|
||||||
|
|
||||||
@@ -161,11 +183,15 @@ async def fetch_calendar_async(headers, progress, task_id, dry_run, vdir_path, i
|
|||||||
next_end_date = next_start_date + timedelta(days=days_forward)
|
next_end_date = next_start_date + timedelta(days=days_forward)
|
||||||
|
|
||||||
progress.console.print(
|
progress.console.print(
|
||||||
f"\nNext date range would be: {next_start_date.strftime(
|
f"\nNext date range would be: {
|
||||||
'%Y-%m-%d')} to {next_end_date.strftime('%Y-%m-%d')}"
|
next_start_date.strftime('%Y-%m-%d')
|
||||||
|
} to {next_end_date.strftime('%Y-%m-%d')}"
|
||||||
|
)
|
||||||
|
user_response = (
|
||||||
|
click.prompt("\nContinue to iterate? [y/N]", default="N")
|
||||||
|
.strip()
|
||||||
|
.lower()
|
||||||
)
|
)
|
||||||
user_response = click.prompt(
|
|
||||||
"\nContinue to iterate? [y/N]", default="N").strip().lower()
|
|
||||||
|
|
||||||
return events
|
return events
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -179,17 +205,23 @@ async def fetch_calendar_async(headers, progress, task_id, dry_run, vdir_path, i
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
async def _sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forward, continue_iteration, download_attachments):
|
async def _sync_outlook_data(
|
||||||
|
dry_run,
|
||||||
|
vdir,
|
||||||
|
icsfile,
|
||||||
|
org,
|
||||||
|
days_back,
|
||||||
|
days_forward,
|
||||||
|
continue_iteration,
|
||||||
|
download_attachments,
|
||||||
|
):
|
||||||
"""Synchronize data from external sources."""
|
"""Synchronize data from external sources."""
|
||||||
|
|
||||||
# Expand the user home directory in vdir path
|
# Expand the user home directory in vdir path
|
||||||
vdir = os.path.expanduser(vdir)
|
vdir = os.path.expanduser(vdir)
|
||||||
|
|
||||||
# Save emails to Maildir
|
# Save emails to Maildir
|
||||||
maildir_path = (
|
maildir_path = os.getenv("MAILDIR_PATH", os.path.expanduser("~/Mail")) + f"/{org}"
|
||||||
os.getenv("MAILDIR_PATH", os.path.expanduser(
|
|
||||||
"~/Mail")) + f"/{org}"
|
|
||||||
)
|
|
||||||
attachments_dir = os.path.join(maildir_path, "attachments")
|
attachments_dir = os.path.join(maildir_path, "attachments")
|
||||||
ensure_directory_exists(attachments_dir)
|
ensure_directory_exists(attachments_dir)
|
||||||
create_maildir_structure(maildir_path)
|
create_maildir_structure(maildir_path)
|
||||||
@@ -210,27 +242,28 @@ async def _sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forwar
|
|||||||
|
|
||||||
with progress:
|
with progress:
|
||||||
task_fetch = progress.add_task("[green]Syncing Inbox...", total=0)
|
task_fetch = progress.add_task("[green]Syncing Inbox...", total=0)
|
||||||
task_calendar = progress.add_task(
|
task_calendar = progress.add_task("[cyan]Fetching calendar...", total=0)
|
||||||
"[cyan]Fetching calendar...", total=0)
|
|
||||||
task_read = progress.add_task("[blue]Marking as read...", total=0)
|
task_read = progress.add_task("[blue]Marking as read...", total=0)
|
||||||
task_archive = progress.add_task("[yellow]Archiving mail...", total=0)
|
task_archive = progress.add_task("[yellow]Archiving mail...", total=0)
|
||||||
task_delete = progress.add_task("[red]Deleting mail...", total=0)
|
task_delete = progress.add_task("[red]Deleting mail...", total=0)
|
||||||
|
|
||||||
# Stage 1: Synchronize local changes (read, archive, delete) to the server
|
# Stage 1: Synchronize local changes (read, archive, delete) to the server
|
||||||
progress.console.print("[bold cyan]Step 1: Syncing local changes to server...[/bold cyan]")
|
progress.console.print(
|
||||||
|
"[bold cyan]Step 1: Syncing local changes to server...[/bold cyan]"
|
||||||
|
)
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
synchronize_maildir_async(
|
synchronize_maildir_async(
|
||||||
maildir_path, headers, progress, task_read, dry_run
|
maildir_path, headers, progress, task_read, dry_run
|
||||||
),
|
),
|
||||||
archive_mail_async(maildir_path, headers,
|
archive_mail_async(maildir_path, headers, progress, task_archive, dry_run),
|
||||||
progress, task_archive, dry_run),
|
delete_mail_async(maildir_path, headers, progress, task_delete, dry_run),
|
||||||
delete_mail_async(maildir_path, headers,
|
|
||||||
progress, task_delete, dry_run),
|
|
||||||
)
|
)
|
||||||
progress.console.print("[bold green]Step 1: Local changes synced.[/bold green]")
|
progress.console.print("[bold green]Step 1: Local changes synced.[/bold green]")
|
||||||
|
|
||||||
# Stage 2: Fetch new data from the server
|
# Stage 2: Fetch new data from the server
|
||||||
progress.console.print("\n[bold cyan]Step 2: Fetching new data from server...[/bold cyan]")
|
progress.console.print(
|
||||||
|
"\n[bold cyan]Step 2: Fetching new data from server...[/bold cyan]"
|
||||||
|
)
|
||||||
await asyncio.gather(
|
await asyncio.gather(
|
||||||
fetch_mail_async(
|
fetch_mail_async(
|
||||||
maildir_path,
|
maildir_path,
|
||||||
@@ -241,7 +274,18 @@ async def _sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forwar
|
|||||||
dry_run,
|
dry_run,
|
||||||
download_attachments,
|
download_attachments,
|
||||||
),
|
),
|
||||||
fetch_calendar_async(headers, progress, task_calendar, dry_run, vdir, icsfile, org, days_back, days_forward, continue_iteration),
|
fetch_calendar_async(
|
||||||
|
headers,
|
||||||
|
progress,
|
||||||
|
task_calendar,
|
||||||
|
dry_run,
|
||||||
|
vdir,
|
||||||
|
icsfile,
|
||||||
|
org,
|
||||||
|
days_back,
|
||||||
|
days_forward,
|
||||||
|
continue_iteration,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
progress.console.print("[bold green]Step 2: New data fetched.[/bold green]")
|
progress.console.print("[bold green]Step 2: New data fetched.[/bold green]")
|
||||||
click.echo("Sync complete.")
|
click.echo("Sync complete.")
|
||||||
@@ -291,5 +335,123 @@ async def _sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forwar
|
|||||||
help="Download email attachments",
|
help="Download email attachments",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
def sync(dry_run, vdir, icsfile, org, days_back, days_forward, continue_iteration, download_attachments):
|
@click.option(
|
||||||
asyncio.run(_sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forward, continue_iteration, download_attachments))
|
"--daemon",
|
||||||
|
is_flag=True,
|
||||||
|
help="Run in daemon mode.",
|
||||||
|
default=False,
|
||||||
|
)
|
||||||
|
def sync(
|
||||||
|
dry_run,
|
||||||
|
vdir,
|
||||||
|
icsfile,
|
||||||
|
org,
|
||||||
|
days_back,
|
||||||
|
days_forward,
|
||||||
|
continue_iteration,
|
||||||
|
download_attachments,
|
||||||
|
daemon,
|
||||||
|
):
|
||||||
|
if daemon:
|
||||||
|
asyncio.run(
|
||||||
|
daemon_mode(
|
||||||
|
dry_run,
|
||||||
|
vdir,
|
||||||
|
icsfile,
|
||||||
|
org,
|
||||||
|
days_back,
|
||||||
|
days_forward,
|
||||||
|
continue_iteration,
|
||||||
|
download_attachments,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
asyncio.run(
|
||||||
|
_sync_outlook_data(
|
||||||
|
dry_run,
|
||||||
|
vdir,
|
||||||
|
icsfile,
|
||||||
|
org,
|
||||||
|
days_back,
|
||||||
|
days_forward,
|
||||||
|
continue_iteration,
|
||||||
|
download_attachments,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def daemon_mode(
|
||||||
|
dry_run,
|
||||||
|
vdir,
|
||||||
|
icsfile,
|
||||||
|
org,
|
||||||
|
days_back,
|
||||||
|
days_forward,
|
||||||
|
continue_iteration,
|
||||||
|
download_attachments,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Run the script in daemon mode, periodically syncing emails.
|
||||||
|
"""
|
||||||
|
from src.services.microsoft_graph.mail import get_inbox_count_async
|
||||||
|
import time
|
||||||
|
|
||||||
|
sync_interval = 300 # 5 minutes
|
||||||
|
check_interval = 10 # 10 seconds
|
||||||
|
last_sync_time = time.time() - sync_interval # Force initial sync
|
||||||
|
|
||||||
|
while True:
|
||||||
|
if time.time() - last_sync_time >= sync_interval:
|
||||||
|
click.echo("[green]Performing full sync...[/green]")
|
||||||
|
# Perform a full sync
|
||||||
|
await _sync_outlook_data(
|
||||||
|
dry_run,
|
||||||
|
vdir,
|
||||||
|
icsfile,
|
||||||
|
org,
|
||||||
|
days_back,
|
||||||
|
days_forward,
|
||||||
|
continue_iteration,
|
||||||
|
download_attachments,
|
||||||
|
)
|
||||||
|
last_sync_time = time.time()
|
||||||
|
else:
|
||||||
|
# Perform a quick check
|
||||||
|
click.echo("[cyan]Checking for new messages...[/cyan]")
|
||||||
|
# Authenticate and get access token
|
||||||
|
scopes = ["https://graph.microsoft.com/Mail.Read"]
|
||||||
|
access_token, headers = get_access_token(scopes)
|
||||||
|
remote_message_count = await get_inbox_count_async(headers)
|
||||||
|
maildir_path = os.path.expanduser(f"~/Mail/{org}")
|
||||||
|
local_message_count = len(
|
||||||
|
[
|
||||||
|
f
|
||||||
|
for f in os.listdir(os.path.join(maildir_path, "new"))
|
||||||
|
if ".eml" in f
|
||||||
|
]
|
||||||
|
) + len(
|
||||||
|
[
|
||||||
|
f
|
||||||
|
for f in os.listdir(os.path.join(maildir_path, "cur"))
|
||||||
|
if ".eml" in f
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if remote_message_count != local_message_count:
|
||||||
|
click.echo(
|
||||||
|
f"[yellow]New messages detected ({remote_message_count} / {local_message_count}), performing full sync...[/yellow]"
|
||||||
|
)
|
||||||
|
await _sync_outlook_data(
|
||||||
|
dry_run,
|
||||||
|
vdir,
|
||||||
|
icsfile,
|
||||||
|
org,
|
||||||
|
days_back,
|
||||||
|
days_forward,
|
||||||
|
continue_iteration,
|
||||||
|
download_attachments,
|
||||||
|
)
|
||||||
|
last_sync_time = time.time()
|
||||||
|
else:
|
||||||
|
click.echo("[green]No new messages detected.[/green]")
|
||||||
|
|
||||||
|
time.sleep(check_interval)
|
||||||
|
|||||||
@@ -116,8 +116,15 @@ async def archive_mail_async(maildir_path, headers, progress, task_id, dry_run=F
|
|||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
"""
|
"""
|
||||||
archive_dir = os.path.join(maildir_path, ".Archives")
|
# Check both possible archive folder names locally
|
||||||
archive_files = glob.glob(os.path.join(archive_dir, "**", "*.eml*"), recursive=True)
|
archive_files = []
|
||||||
|
for archive_folder_name in [".Archives", ".Archive"]:
|
||||||
|
archive_dir = os.path.join(maildir_path, archive_folder_name)
|
||||||
|
if os.path.exists(archive_dir):
|
||||||
|
archive_files.extend(
|
||||||
|
glob.glob(os.path.join(archive_dir, "**", "*.eml*"), recursive=True)
|
||||||
|
)
|
||||||
|
|
||||||
progress.update(task_id, total=len(archive_files))
|
progress.update(task_id, total=len(archive_files))
|
||||||
|
|
||||||
folder_response = await fetch_with_aiohttp(
|
folder_response = await fetch_with_aiohttp(
|
||||||
@@ -128,13 +135,13 @@ async def archive_mail_async(maildir_path, headers, progress, task_id, dry_run=F
|
|||||||
(
|
(
|
||||||
folder.get("id")
|
folder.get("id")
|
||||||
for folder in folders
|
for folder in folders
|
||||||
if folder.get("displayName", "").lower() == "archive"
|
if folder.get("displayName", "").lower() in ["archive", "archives"]
|
||||||
),
|
),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not archive_folder_id:
|
if not archive_folder_id:
|
||||||
raise Exception("No folder named 'Archive' found on the server.")
|
raise Exception("No folder named 'Archive' or 'Archives' found on the server.")
|
||||||
|
|
||||||
for filepath in archive_files:
|
for filepath in archive_files:
|
||||||
message_id = os.path.basename(filepath).split(".")[
|
message_id = os.path.basename(filepath).split(".")[
|
||||||
@@ -147,17 +154,22 @@ async def archive_mail_async(maildir_path, headers, progress, task_id, dry_run=F
|
|||||||
headers,
|
headers,
|
||||||
{"destinationId": archive_folder_id},
|
{"destinationId": archive_folder_id},
|
||||||
)
|
)
|
||||||
if status != 201: # 201 Created indicates success
|
if status == 201: # 201 Created indicates successful move
|
||||||
progress.console.print(
|
os.remove(
|
||||||
f"Failed to move message to 'Archive': {message_id}, {status}"
|
filepath
|
||||||
)
|
) # Remove the local file since it's now archived on server
|
||||||
if status == 404:
|
progress.console.print(f"Moved message to 'Archive': {message_id}")
|
||||||
os.remove(filepath) # Remove the file from local archive if not found
|
elif status == 404:
|
||||||
|
os.remove(
|
||||||
|
filepath
|
||||||
|
) # Remove the file from local archive if not found on server
|
||||||
progress.console.print(
|
progress.console.print(
|
||||||
f"Message not found on server, removed local copy: {message_id}"
|
f"Message not found on server, removed local copy: {message_id}"
|
||||||
)
|
)
|
||||||
elif status == 204:
|
else:
|
||||||
progress.console.print(f"Moved message to 'Archive': {message_id}")
|
progress.console.print(
|
||||||
|
f"Failed to move message to 'Archive': {message_id}, status: {status}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
progress.console.print(
|
progress.console.print(
|
||||||
f"[DRY-RUN] Would move message to 'Archive' folder: {message_id}"
|
f"[DRY-RUN] Would move message to 'Archive' folder: {message_id}"
|
||||||
@@ -200,6 +212,21 @@ async def delete_mail_async(maildir_path, headers, progress, task_id, dry_run=Fa
|
|||||||
progress.advance(task_id)
|
progress.advance(task_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_inbox_count_async(headers):
|
||||||
|
"""
|
||||||
|
Get the number of messages in the inbox.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
headers (dict): Headers including authentication.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
int: The number of messages in the inbox.
|
||||||
|
"""
|
||||||
|
inbox_url = "https://graph.microsoft.com/v1.0/me/mailFolders/inbox"
|
||||||
|
response = await fetch_with_aiohttp(inbox_url, headers)
|
||||||
|
return response.get("totalItemCount", 0)
|
||||||
|
|
||||||
|
|
||||||
async def synchronize_maildir_async(
|
async def synchronize_maildir_async(
|
||||||
maildir_path, headers, progress, task_id, dry_run=False
|
maildir_path, headers, progress, task_id, dry_run=False
|
||||||
):
|
):
|
||||||
@@ -220,7 +247,7 @@ async def synchronize_maildir_async(
|
|||||||
load_last_sync_timestamp,
|
load_last_sync_timestamp,
|
||||||
save_sync_timestamp,
|
save_sync_timestamp,
|
||||||
truncate_id,
|
truncate_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
last_sync = load_last_sync_timestamp()
|
last_sync = load_last_sync_timestamp()
|
||||||
|
|
||||||
|
|||||||
0
tests/fixtures/envelope_list_empty.txt
vendored
Normal file
0
tests/fixtures/envelope_list_empty.txt
vendored
Normal file
6
tests/fixtures/envelope_list_normal.txt
vendored
Normal file
6
tests/fixtures/envelope_list_normal.txt
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 1 | | Important Meeting | john@example.com | 2024-01-15 10:00+00:00|
|
||||||
|
| 5 | * | Project Update | sarah@company.com | 2024-01-14 15:30+00:00|
|
||||||
|
| 7 | | Weekly Standup | team@startup.com | 2024-01-13 09:00+00:00|
|
||||||
|
| 10 | | Contract Review | client@business.com | 2024-01-12 14:00+00:00|
|
||||||
3
tests/fixtures/envelope_list_single.txt
vendored
Normal file
3
tests/fixtures/envelope_list_single.txt
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 42 | | Only Message | single@example.com | 2024-01-15 10:00+00:00|
|
||||||
11
tests/fixtures/message_content_1.txt
vendored
Normal file
11
tests/fixtures/message_content_1.txt
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
From: john@example.com
|
||||||
|
To: user@example.com
|
||||||
|
Subject: Important Meeting
|
||||||
|
Date: 2024-01-15
|
||||||
|
|
||||||
|
Hi there,
|
||||||
|
|
||||||
|
We need to schedule an important meeting for next week. Please let me know your availability.
|
||||||
|
|
||||||
|
Best regards,
|
||||||
|
John
|
||||||
1
tests/himalaya
Symbolic link
1
tests/himalaya
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
mock_himalaya.sh
|
||||||
152
tests/integration_tests.sh
Executable file
152
tests/integration_tests.sh
Executable file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Integration tests for run_himalaya.sh
|
||||||
|
|
||||||
|
# Source test utilities
|
||||||
|
source "$(dirname "$0")/test_utils.sh"
|
||||||
|
|
||||||
|
# Make sure we can find the mock himalaya command
|
||||||
|
export PATH="$(dirname "$0"):$PATH"
|
||||||
|
|
||||||
|
# Common test data in real himalaya format
|
||||||
|
NORMAL_ENVELOPE_LIST="| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 1 | | Important Meeting | john@example.com | 2024-01-15 10:00+00:00|
|
||||||
|
| 5 | * | Project Update | sarah@company.com | 2024-01-14 15:30+00:00|"
|
||||||
|
|
||||||
|
EXTENDED_ENVELOPE_LIST="| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 1 | | Important Meeting | john@example.com | 2024-01-15 10:00+00:00|
|
||||||
|
| 5 | * | Project Update | sarah@company.com | 2024-01-14 15:30+00:00|
|
||||||
|
| 7 | | Weekly Standup | team@startup.com | 2024-01-13 09:00+00:00|"
|
||||||
|
|
||||||
|
# Test: Script with no arguments should auto-discover latest message
|
||||||
|
test_script_auto_discover() {
|
||||||
|
export MOCK_ENVELOPE_LIST="$NORMAL_ENVELOPE_LIST"
|
||||||
|
|
||||||
|
# Simulate user input: quit immediately
|
||||||
|
local output=$(echo "q" | timeout 10 ./run_himalaya.sh 2>/dev/null)
|
||||||
|
local exit_code=$?
|
||||||
|
|
||||||
|
# Check that it found the latest message (5) and attempted to read it
|
||||||
|
if echo "$output" | grep -q "Latest message ID: 5" && echo "$output" | grep -q "Reading message 5"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "Expected auto-discovery of message 5, got: $output"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: Script with valid message ID
|
||||||
|
test_script_valid_id() {
|
||||||
|
export MOCK_ENVELOPE_LIST="$NORMAL_ENVELOPE_LIST"
|
||||||
|
|
||||||
|
# Simulate user input: quit immediately
|
||||||
|
local output=$(echo "q" | timeout 10 ./run_himalaya.sh 1 2>/dev/null)
|
||||||
|
|
||||||
|
# Check that it read the specified message
|
||||||
|
if echo "$output" | grep -q "Reading message 1"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "Expected reading of message 1, got: $output"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: Script with invalid message ID should fallback to latest
|
||||||
|
test_script_invalid_id_fallback() {
|
||||||
|
export MOCK_ENVELOPE_LIST="$NORMAL_ENVELOPE_LIST"
|
||||||
|
export MOCK_INVALID_MESSAGE_ID="99"
|
||||||
|
|
||||||
|
# Simulate user input: quit immediately
|
||||||
|
local output=$(echo "q" | timeout 10 ./run_himalaya.sh 99 2>/dev/null)
|
||||||
|
|
||||||
|
# Check that it fell back to latest message
|
||||||
|
if echo "$output" | grep -q "Opening latest message: 5"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "Expected fallback to latest message, got: $output"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: Empty inbox handling
|
||||||
|
test_script_empty_inbox() {
|
||||||
|
export MOCK_EMPTY_INBOX="true"
|
||||||
|
|
||||||
|
# Don't use timeout as it may interfere with exit codes
|
||||||
|
# Use bash -c to avoid subshell exit code issues
|
||||||
|
bash -c './run_himalaya.sh' > "$TEST_TEMP_DIR/output.txt" 2>&1
|
||||||
|
local exit_code=$?
|
||||||
|
local output=$(cat "$TEST_TEMP_DIR/output.txt")
|
||||||
|
|
||||||
|
# Should exit with error message about no messages and exit code 1
|
||||||
|
if echo "$output" | grep -q "No messages found in inbox" && [ $exit_code -eq 1 ]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "Expected 'No messages found' error with exit code 1, got: $output (exit code: $exit_code)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: Navigation to next message
|
||||||
|
test_navigation_next() {
|
||||||
|
export MOCK_ENVELOPE_LIST="$EXTENDED_ENVELOPE_LIST"
|
||||||
|
|
||||||
|
# Simulate user input: next message, then quit
|
||||||
|
local output=$(printf "n\nq\n" | timeout 10 ./run_himalaya.sh 1 2>/dev/null)
|
||||||
|
|
||||||
|
# Check that it moved to next message (5)
|
||||||
|
if echo "$output" | grep -q "Opening next message: 5" && echo "$output" | grep -q "Reading message 5"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "Expected navigation to next message 5, got: $output"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: Navigation to previous message
|
||||||
|
test_navigation_previous() {
|
||||||
|
export MOCK_ENVELOPE_LIST="$EXTENDED_ENVELOPE_LIST"
|
||||||
|
|
||||||
|
# Simulate user input: previous message, then quit
|
||||||
|
local output=$(printf "p\nq\n" | timeout 10 ./run_himalaya.sh 7 2>/dev/null)
|
||||||
|
|
||||||
|
# Check that it moved to previous message (5)
|
||||||
|
if echo "$output" | grep -q "Opening previous message: 5" && echo "$output" | grep -q "Reading message 5"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "Expected navigation to previous message 5, got: $output"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: No next message available
|
||||||
|
test_navigation_no_next() {
|
||||||
|
export MOCK_ENVELOPE_LIST="$NORMAL_ENVELOPE_LIST"
|
||||||
|
|
||||||
|
# Simulate user input: next message (should fail), then quit
|
||||||
|
local output=$(printf "n\nq\n" | timeout 10 ./run_himalaya.sh 5 2>/dev/null)
|
||||||
|
|
||||||
|
# Check that it shows "No next message available"
|
||||||
|
if echo "$output" | grep -q "No next message available"; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo "Expected 'No next message available', got: $output"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
echo "Running integration tests for run_himalaya.sh..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
run_test "Script auto-discovers latest message when no args" test_script_auto_discover
|
||||||
|
run_test "Script reads specified valid message ID" test_script_valid_id
|
||||||
|
run_test "Script falls back to latest when invalid ID provided" test_script_invalid_id_fallback
|
||||||
|
run_test "Script handles empty inbox gracefully" test_script_empty_inbox
|
||||||
|
run_test "Navigation to next message works" test_navigation_next
|
||||||
|
run_test "Navigation to previous message works" test_navigation_previous
|
||||||
|
run_test "No next message available handled gracefully" test_navigation_no_next
|
||||||
|
|
||||||
|
print_test_summary
|
||||||
101
tests/mock_himalaya.sh
Executable file
101
tests/mock_himalaya.sh
Executable file
@@ -0,0 +1,101 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Mock himalaya command for testing
|
||||||
|
# Controlled by environment variables for different test scenarios
|
||||||
|
|
||||||
|
# Mock less command that doesn't wait for input in tests
|
||||||
|
if [ -z "$MOCK_LESS_INTERACTIVE" ]; then
|
||||||
|
# In test mode, just output directly without paging
|
||||||
|
alias less='cat'
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Default test data - matches real himalaya output format
|
||||||
|
DEFAULT_ENVELOPE_LIST="| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 1 | | Important Meeting | john@example.com | 2024-01-15 10:00+00:00|
|
||||||
|
| 5 | * | Project Update | sarah@company.com | 2024-01-14 15:30+00:00|
|
||||||
|
| 7 | | Weekly Standup | team@startup.com | 2024-01-13 09:00+00:00|
|
||||||
|
| 10 | | Contract Review | client@business.com | 2024-01-12 14:00+00:00|"
|
||||||
|
|
||||||
|
DEFAULT_MESSAGE_CONTENT="From: test@example.com
|
||||||
|
To: user@example.com
|
||||||
|
Subject: Test Message
|
||||||
|
Date: 2024-01-15
|
||||||
|
|
||||||
|
This is a test message content for testing purposes."
|
||||||
|
|
||||||
|
# Handle different commands
|
||||||
|
case "$1" in
|
||||||
|
"envelope")
|
||||||
|
case "$2" in
|
||||||
|
"list")
|
||||||
|
if [ -n "$MOCK_ENVELOPE_LIST" ]; then
|
||||||
|
echo "$MOCK_ENVELOPE_LIST"
|
||||||
|
elif [ "$MOCK_EMPTY_INBOX" = "true" ]; then
|
||||||
|
echo ""
|
||||||
|
elif [ "$MOCK_HIMALAYA_FAIL" = "true" ]; then
|
||||||
|
echo "Error: Unable to connect to server" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "$DEFAULT_ENVELOPE_LIST"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
"message")
|
||||||
|
case "$2" in
|
||||||
|
"read")
|
||||||
|
message_id="$3"
|
||||||
|
if [ "$MOCK_HIMALAYA_FAIL" = "true" ]; then
|
||||||
|
echo "Error: Unable to read message $message_id" >&2
|
||||||
|
exit 1
|
||||||
|
elif [ -n "$MOCK_INVALID_MESSAGE_ID" ] && [ "$message_id" = "$MOCK_INVALID_MESSAGE_ID" ]; then
|
||||||
|
echo "Error: Message $message_id not found" >&2
|
||||||
|
exit 1
|
||||||
|
elif [ -n "$MOCK_ENVELOPE_LIST" ]; then
|
||||||
|
# Check if the message ID exists in our envelope list
|
||||||
|
# Handle both old tab-separated format and new table format
|
||||||
|
if echo "$MOCK_ENVELOPE_LIST" | grep -q "^$message_id " || echo "$MOCK_ENVELOPE_LIST" | grep -q "^| $message_id "; then
|
||||||
|
if [ -n "$MOCK_MESSAGE_CONTENT" ]; then
|
||||||
|
echo "$MOCK_MESSAGE_CONTENT"
|
||||||
|
else
|
||||||
|
echo "$DEFAULT_MESSAGE_CONTENT"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Error: Message $message_id not found" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [ -n "$MOCK_MESSAGE_CONTENT" ]; then
|
||||||
|
echo "$MOCK_MESSAGE_CONTENT"
|
||||||
|
else
|
||||||
|
echo "$DEFAULT_MESSAGE_CONTENT"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"delete")
|
||||||
|
message_id="$3"
|
||||||
|
if [ "$MOCK_HIMALAYA_FAIL" = "true" ]; then
|
||||||
|
echo "Error: Unable to delete message $message_id" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Message $message_id deleted successfully"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
"move")
|
||||||
|
folder="$3"
|
||||||
|
message_id="$4"
|
||||||
|
if [ "$MOCK_HIMALAYA_FAIL" = "true" ]; then
|
||||||
|
echo "Error: Unable to move message $message_id to $folder" >&2
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Message $message_id moved to $folder successfully"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Mock himalaya: Unknown command $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
132
tests/test_utils.sh
Normal file
132
tests/test_utils.sh
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Test utilities for run_himalaya.sh testing
|
||||||
|
|
||||||
|
# Colors for test output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Test counters
|
||||||
|
TESTS_RUN=0
|
||||||
|
TESTS_PASSED=0
|
||||||
|
TESTS_FAILED=0
|
||||||
|
|
||||||
|
# Setup function - prepares test environment
|
||||||
|
setup_test() {
|
||||||
|
# Clear any existing mock environment variables
|
||||||
|
unset MOCK_ENVELOPE_LIST
|
||||||
|
unset MOCK_MESSAGE_CONTENT
|
||||||
|
unset MOCK_EMPTY_INBOX
|
||||||
|
unset MOCK_HIMALAYA_FAIL
|
||||||
|
unset MOCK_INVALID_MESSAGE_ID
|
||||||
|
|
||||||
|
# Set up PATH to use mock himalaya
|
||||||
|
export PATH="$(pwd)/tests:$PATH"
|
||||||
|
|
||||||
|
# Create temporary files for testing
|
||||||
|
export TEST_TEMP_DIR="/tmp/himalaya_test_$$"
|
||||||
|
mkdir -p "$TEST_TEMP_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup function
|
||||||
|
cleanup_test() {
|
||||||
|
# Remove temporary files
|
||||||
|
if [ -n "$TEST_TEMP_DIR" ] && [ -d "$TEST_TEMP_DIR" ]; then
|
||||||
|
rm -rf "$TEST_TEMP_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reset PATH
|
||||||
|
export PATH=$(echo "$PATH" | sed "s|$(pwd)/tests:||")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to simulate user input
|
||||||
|
simulate_input() {
|
||||||
|
local input="$1"
|
||||||
|
echo "$input"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to run a test
|
||||||
|
run_test() {
|
||||||
|
local test_name="$1"
|
||||||
|
local test_function="$2"
|
||||||
|
|
||||||
|
echo -e "${YELLOW}Running: $test_name${NC}"
|
||||||
|
TESTS_RUN=$((TESTS_RUN + 1))
|
||||||
|
|
||||||
|
setup_test
|
||||||
|
|
||||||
|
if $test_function; then
|
||||||
|
echo -e "${GREEN}✓ PASS: $test_name${NC}"
|
||||||
|
TESTS_PASSED=$((TESTS_PASSED + 1))
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ FAIL: $test_name${NC}"
|
||||||
|
TESTS_FAILED=$((TESTS_FAILED + 1))
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_test
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to assert equality
|
||||||
|
assert_equals() {
|
||||||
|
local expected="$1"
|
||||||
|
local actual="$2"
|
||||||
|
local message="$3"
|
||||||
|
|
||||||
|
if [ "$expected" = "$actual" ]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}Assertion failed: $message${NC}"
|
||||||
|
echo -e "${RED}Expected: '$expected'${NC}"
|
||||||
|
echo -e "${RED}Actual: '$actual'${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to assert command success
|
||||||
|
assert_success() {
|
||||||
|
local command="$1"
|
||||||
|
local message="$2"
|
||||||
|
|
||||||
|
if eval "$command" >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}Command failed: $message${NC}"
|
||||||
|
echo -e "${RED}Command: $command${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to assert command failure
|
||||||
|
assert_failure() {
|
||||||
|
local command="$1"
|
||||||
|
local message="$2"
|
||||||
|
|
||||||
|
if eval "$command" >/dev/null 2>&1; then
|
||||||
|
echo -e "${RED}Command unexpectedly succeeded: $message${NC}"
|
||||||
|
echo -e "${RED}Command: $command${NC}"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to print test summary
|
||||||
|
print_test_summary() {
|
||||||
|
echo "===================="
|
||||||
|
echo "Test Summary"
|
||||||
|
echo "===================="
|
||||||
|
echo "Tests run: $TESTS_RUN"
|
||||||
|
echo -e "Passed: ${GREEN}$TESTS_PASSED${NC}"
|
||||||
|
echo -e "Failed: ${RED}$TESTS_FAILED${NC}"
|
||||||
|
|
||||||
|
if [ $TESTS_FAILED -eq 0 ]; then
|
||||||
|
echo -e "${GREEN}All tests passed!${NC}"
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
echo -e "${RED}Some tests failed!${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
165
tests/unit_tests.sh
Executable file
165
tests/unit_tests.sh
Executable file
@@ -0,0 +1,165 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Unit tests for run_himalaya.sh functions
|
||||||
|
|
||||||
|
# Source test utilities
|
||||||
|
source "$(dirname "$0")/test_utils.sh"
|
||||||
|
|
||||||
|
# Make sure we can find the mock himalaya command
|
||||||
|
export PATH="$(dirname "$0"):$PATH"
|
||||||
|
|
||||||
|
# Source the functions from run_himalaya.sh for testing
|
||||||
|
# We need to extract just the functions for testing
|
||||||
|
create_function_file() {
|
||||||
|
# Create a temporary file with just the functions
|
||||||
|
cat > "$TEST_TEMP_DIR/functions.sh" << 'EOFUNC'
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Function to get available message IDs from himalaya
|
||||||
|
get_available_message_ids() {
|
||||||
|
himalaya envelope list | awk 'NR > 2 && /^\| [0-9]/ {gsub(/[| ]/, "", $2); if($2 ~ /^[0-9]+$/) print $2}' | sort -n
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to get the latest (most recent) message ID
|
||||||
|
get_latest_message_id() {
|
||||||
|
get_available_message_ids | tail -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to find the next valid message ID
|
||||||
|
find_next_message_id() {
|
||||||
|
local current_id="$1"
|
||||||
|
get_available_message_ids | awk -v current="$current_id" '$1 > current {print $1; exit}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to find the previous valid message ID
|
||||||
|
find_previous_message_id() {
|
||||||
|
local current_id="$1"
|
||||||
|
get_available_message_ids | awk -v current="$current_id" '$1 < current {prev=$1} END {if(prev) print prev}'
|
||||||
|
}
|
||||||
|
EOFUNC
|
||||||
|
source "$TEST_TEMP_DIR/functions.sh"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: get_available_message_ids with normal data
|
||||||
|
test_get_available_message_ids_normal() {
|
||||||
|
export MOCK_ENVELOPE_LIST="| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 1 | | Important Meeting | john@example.com | 2024-01-15 10:00+00:00|
|
||||||
|
| 5 | * | Project Update | sarah@company.com | 2024-01-14 15:30+00:00|
|
||||||
|
| 7 | | Weekly Standup | team@startup.com | 2024-01-13 09:00+00:00|
|
||||||
|
| 10 | | Contract Review | client@business.com | 2024-01-12 14:00+00:00|"
|
||||||
|
|
||||||
|
create_function_file
|
||||||
|
local result=$(get_available_message_ids | tr '\n' ' ')
|
||||||
|
local expected="1 5 7 10 "
|
||||||
|
|
||||||
|
assert_equals "$expected" "$result" "Should return sorted message IDs"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: get_available_message_ids with empty inbox
|
||||||
|
test_get_available_message_ids_empty() {
|
||||||
|
export MOCK_EMPTY_INBOX="true"
|
||||||
|
|
||||||
|
create_function_file
|
||||||
|
local result=$(get_available_message_ids)
|
||||||
|
|
||||||
|
assert_equals "" "$result" "Should return empty for empty inbox"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: get_latest_message_id with normal data
|
||||||
|
test_get_latest_message_id_normal() {
|
||||||
|
export MOCK_ENVELOPE_LIST="| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 1 | | Important Meeting | john@example.com | 2024-01-15 10:00+00:00|
|
||||||
|
| 5 | * | Project Update | sarah@company.com | 2024-01-14 15:30+00:00|
|
||||||
|
| 7 | | Weekly Standup | team@startup.com | 2024-01-13 09:00+00:00|
|
||||||
|
| 10 | | Contract Review | client@business.com | 2024-01-12 14:00+00:00|"
|
||||||
|
|
||||||
|
create_function_file
|
||||||
|
local result=$(get_latest_message_id)
|
||||||
|
|
||||||
|
assert_equals "10" "$result" "Should return the highest message ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: get_latest_message_id with single message
|
||||||
|
test_get_latest_message_id_single() {
|
||||||
|
export MOCK_ENVELOPE_LIST="| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 42 | | Only Message | single@example.com | 2024-01-15 10:00+00:00|"
|
||||||
|
|
||||||
|
create_function_file
|
||||||
|
local result=$(get_latest_message_id)
|
||||||
|
|
||||||
|
assert_equals "42" "$result" "Should return the single message ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: find_next_message_id with valid next
|
||||||
|
test_find_next_message_id_valid() {
|
||||||
|
export MOCK_ENVELOPE_LIST="| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 1 | | Important Meeting | john@example.com | 2024-01-15 10:00+00:00|
|
||||||
|
| 5 | * | Project Update | sarah@company.com | 2024-01-14 15:30+00:00|
|
||||||
|
| 7 | | Weekly Standup | team@startup.com | 2024-01-13 09:00+00:00|
|
||||||
|
| 10 | | Contract Review | client@business.com | 2024-01-12 14:00+00:00|"
|
||||||
|
|
||||||
|
create_function_file
|
||||||
|
local result=$(find_next_message_id "5")
|
||||||
|
|
||||||
|
assert_equals "7" "$result" "Should return next available message ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: find_next_message_id with no next available
|
||||||
|
test_find_next_message_id_none() {
|
||||||
|
export MOCK_ENVELOPE_LIST="| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 1 | | Important Meeting | john@example.com | 2024-01-15 10:00+00:00|
|
||||||
|
| 5 | * | Project Update | sarah@company.com | 2024-01-14 15:30+00:00|"
|
||||||
|
|
||||||
|
create_function_file
|
||||||
|
local result=$(find_next_message_id "5")
|
||||||
|
|
||||||
|
assert_equals "" "$result" "Should return empty when no next message"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: find_previous_message_id with valid previous
|
||||||
|
test_find_previous_message_id_valid() {
|
||||||
|
export MOCK_ENVELOPE_LIST="| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 1 | | Important Meeting | john@example.com | 2024-01-15 10:00+00:00|
|
||||||
|
| 5 | * | Project Update | sarah@company.com | 2024-01-14 15:30+00:00|
|
||||||
|
| 7 | | Weekly Standup | team@startup.com | 2024-01-13 09:00+00:00|
|
||||||
|
| 10 | | Contract Review | client@business.com | 2024-01-12 14:00+00:00|"
|
||||||
|
|
||||||
|
create_function_file
|
||||||
|
local result=$(find_previous_message_id "7")
|
||||||
|
|
||||||
|
assert_equals "5" "$result" "Should return previous available message ID"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test: find_previous_message_id with no previous available
|
||||||
|
test_find_previous_message_id_none() {
|
||||||
|
export MOCK_ENVELOPE_LIST="| ID | FLAGS | SUBJECT | FROM | DATE |
|
||||||
|
|------|-------|---------------------------------------------------------------------------------------------------|------------------------------|------------------------|
|
||||||
|
| 5 | * | Project Update | sarah@company.com | 2024-01-14 15:30+00:00|
|
||||||
|
| 7 | | Weekly Standup | team@startup.com | 2024-01-13 09:00+00:00|"
|
||||||
|
|
||||||
|
create_function_file
|
||||||
|
local result=$(find_previous_message_id "5")
|
||||||
|
|
||||||
|
assert_equals "" "$result" "Should return empty when no previous message"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run all tests
|
||||||
|
echo "Running unit tests for run_himalaya.sh functions..."
|
||||||
|
echo
|
||||||
|
|
||||||
|
run_test "get_available_message_ids with normal data" test_get_available_message_ids_normal
|
||||||
|
run_test "get_available_message_ids with empty inbox" test_get_available_message_ids_empty
|
||||||
|
run_test "get_latest_message_id with normal data" test_get_latest_message_id_normal
|
||||||
|
run_test "get_latest_message_id with single message" test_get_latest_message_id_single
|
||||||
|
run_test "find_next_message_id with valid next" test_find_next_message_id_valid
|
||||||
|
run_test "find_next_message_id with no next available" test_find_next_message_id_none
|
||||||
|
run_test "find_previous_message_id with valid previous" test_find_previous_message_id_valid
|
||||||
|
run_test "find_previous_message_id with no previous available" test_find_previous_message_id_none
|
||||||
|
|
||||||
|
print_test_summary
|
||||||
Reference in New Issue
Block a user