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
|
||||
|
||||
|
||||
# Function to create Maildir structure
|
||||
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"))
|
||||
|
||||
|
||||
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.
|
||||
|
||||
@@ -73,8 +85,7 @@ async def fetch_calendar_async(headers, progress, task_id, dry_run, vdir_path, i
|
||||
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)
|
||||
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]"
|
||||
)
|
||||
@@ -97,7 +108,8 @@ async def fetch_calendar_async(headers, progress, task_id, dry_run, vdir_path, i
|
||||
else:
|
||||
progress.console.print(
|
||||
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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
progress.console.print(
|
||||
f"\nCurrent date range: {next_start_date.strftime(
|
||||
'%Y-%m-%d')} to {next_end_date.strftime('%Y-%m-%d')}"
|
||||
f"\nCurrent date range: {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()
|
||||
)
|
||||
|
||||
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')}..."
|
||||
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
|
||||
@@ -152,7 +169,12 @@ async def fetch_calendar_async(headers, progress, task_id, dry_run, vdir_path, i
|
||||
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'}"
|
||||
'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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
progress.console.print(
|
||||
f"\nNext date range would be: {next_start_date.strftime(
|
||||
'%Y-%m-%d')} to {next_end_date.strftime('%Y-%m-%d')}"
|
||||
f"\nNext date range would be: {
|
||||
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
|
||||
except Exception as e:
|
||||
@@ -179,17 +205,23 @@ async def fetch_calendar_async(headers, progress, task_id, dry_run, vdir_path, i
|
||||
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."""
|
||||
|
||||
# Expand the user home directory in vdir path
|
||||
vdir = os.path.expanduser(vdir)
|
||||
|
||||
# Save emails to Maildir
|
||||
maildir_path = (
|
||||
os.getenv("MAILDIR_PATH", os.path.expanduser(
|
||||
"~/Mail")) + f"/{org}"
|
||||
)
|
||||
maildir_path = os.getenv("MAILDIR_PATH", os.path.expanduser("~/Mail")) + f"/{org}"
|
||||
attachments_dir = os.path.join(maildir_path, "attachments")
|
||||
ensure_directory_exists(attachments_dir)
|
||||
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:
|
||||
task_fetch = progress.add_task("[green]Syncing Inbox...", total=0)
|
||||
task_calendar = progress.add_task(
|
||||
"[cyan]Fetching calendar...", 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)
|
||||
|
||||
# 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(
|
||||
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),
|
||||
archive_mail_async(maildir_path, headers, progress, task_archive, 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]")
|
||||
|
||||
# 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(
|
||||
fetch_mail_async(
|
||||
maildir_path,
|
||||
@@ -241,7 +274,18 @@ async def _sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forwar
|
||||
dry_run,
|
||||
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]")
|
||||
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",
|
||||
default=False,
|
||||
)
|
||||
def sync(dry_run, vdir, icsfile, org, days_back, days_forward, continue_iteration, download_attachments):
|
||||
asyncio.run(_sync_outlook_data(dry_run, vdir, icsfile, org, days_back, days_forward, continue_iteration, download_attachments))
|
||||
@click.option(
|
||||
"--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:
|
||||
None
|
||||
"""
|
||||
archive_dir = os.path.join(maildir_path, ".Archives")
|
||||
archive_files = glob.glob(os.path.join(archive_dir, "**", "*.eml*"), recursive=True)
|
||||
# Check both possible archive folder names locally
|
||||
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))
|
||||
|
||||
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")
|
||||
for folder in folders
|
||||
if folder.get("displayName", "").lower() == "archive"
|
||||
if folder.get("displayName", "").lower() in ["archive", "archives"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
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:
|
||||
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,
|
||||
{"destinationId": archive_folder_id},
|
||||
)
|
||||
if status != 201: # 201 Created indicates success
|
||||
progress.console.print(
|
||||
f"Failed to move message to 'Archive': {message_id}, {status}"
|
||||
)
|
||||
if status == 404:
|
||||
os.remove(filepath) # Remove the file from local archive if not found
|
||||
if status == 201: # 201 Created indicates successful move
|
||||
os.remove(
|
||||
filepath
|
||||
) # Remove the local file since it's now archived on server
|
||||
progress.console.print(f"Moved message to 'Archive': {message_id}")
|
||||
elif status == 404:
|
||||
os.remove(
|
||||
filepath
|
||||
) # Remove the file from local archive if not found on server
|
||||
progress.console.print(
|
||||
f"Message not found on server, removed local copy: {message_id}"
|
||||
)
|
||||
elif status == 204:
|
||||
progress.console.print(f"Moved message to 'Archive': {message_id}")
|
||||
else:
|
||||
progress.console.print(
|
||||
f"Failed to move message to 'Archive': {message_id}, status: {status}"
|
||||
)
|
||||
else:
|
||||
progress.console.print(
|
||||
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)
|
||||
|
||||
|
||||
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(
|
||||
maildir_path, headers, progress, task_id, dry_run=False
|
||||
):
|
||||
@@ -217,10 +244,10 @@ async def synchronize_maildir_async(
|
||||
None
|
||||
"""
|
||||
from src.utils.mail_utils.helpers import (
|
||||
load_last_sync_timestamp,
|
||||
save_sync_timestamp,
|
||||
truncate_id,
|
||||
)
|
||||
load_last_sync_timestamp,
|
||||
save_sync_timestamp,
|
||||
truncate_id,
|
||||
)
|
||||
|
||||
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