Action

Call Sheet

Posted by David Degner, Last update 8 days ago

UPDATES

8 days ago

Now Gemini based and reconstructs the email thread through gemini instead of programatically.

show all updates...

8 days ago

Now Gemini based and reconstructs the email thread through gemini instead of programatically.

3 months ago

Update for the latest Mail app. Now selects all emails in conversation based on subject.

6 months ago

Fixed email link finally

7 months ago

Fixed the return/linefeed error again

7 months ago

Replaced the shell script which appears to fix the beach-balling delay.

7 months ago

Updating the name

7 months ago

Fix text and MarkDown

7 months ago

Converted the output to Markdown

7 months ago

Fixed the Long Title bug through Return character handling

Changed the URL encoding to use AppleScript instead of Python for eventual complete removal of Python

Removed Temporary file cleanup to try and speed up the action

Changed OpenAI model to gpt-4o for speed

7 months ago

Here’s an enumeration of the changes and improvements between V1 and V4 of the email thread processor script:

  1. Improved removeQuotedText function:

    • Added a more sophisticated quote detection system using patterns
    • Introduced a quoteHeaderPattern to identify and skip quote headers
    • Improved handling of different quote formats
  2. New helper functions:

    • Added a trim function to remove leading and trailing whitespace
    • Introduced a matchesPattern function for regex-like pattern matching
  3. Enhanced email processing:

    • Now includes the creation of message links for each email in the thread
    • Improved formatting of email details in the threadContent
  4. Cleanup functionality:

    • Added a cleanupTempFiles function to remove temporary files created during processing
  5. Error handling:

    • Improved error handling and user feedback throughout the script
  6. Prompt improvements:

    • Updated the prompt text with more detailed instructions
    • Added a requirement to include section headings even for empty sections
  7. File handling:

    • Improved temporary file naming convention (using “email_processor_” prefix)
    • Better management of file paths and cleanup
  8. Script structure:

    • Reorganized the script into more clearly defined functions
    • Added comments to improve code readability and maintainability
  9. Execution flow:

    • Added an execute function to encapsulate the main script logic
    • The script now runs the execute function automatically when launched
  10. Minor adjustments:

    • Updated variable names for clarity
    • Improved string concatenation and formatting throughout the script
  11. Compatibility:

    • Explicitly mentioned compatibility with modern MacOS and Python3

These improvements make the V4 script more robust, efficient, and user-friendly compared to the V1 version, with better handling of email threads and improved information extraction capabilities.

7 months ago

Here’s an enumeration of the changes and improvements between V1 and V4 of the email thread processor script:

  1. Improved removeQuotedText function:

    • Added a more sophisticated quote detection system using patterns
    • Introduced a quoteHeaderPattern to identify and skip quote headers
    • Improved handling of different quote formats
  2. New helper functions:

    • Added a trim function to remove leading and trailing whitespace
    • Introduced a matchesPattern function for regex-like pattern matching
  3. Enhanced email processing:

    • Now includes the creation of message links for each email in the thread
    • Improved formatting of email details in the threadContent
  4. Cleanup functionality:

    • Added a cleanupTempFiles function to remove temporary files created during processing
  5. Error handling:

    • Improved error handling and user feedback throughout the script
  6. Prompt improvements:

    • Updated the prompt text with more detailed instructions
    • Added a requirement to include section headings even for empty sections
  7. File handling:

    • Improved temporary file naming convention (using “email_processor_” prefix)
    • Better management of file paths and cleanup
  8. Script structure:

    • Reorganized the script into more clearly defined functions
    • Added comments to improve code readability and maintainability
  9. Execution flow:

    • Added an execute function to encapsulate the main script logic
    • The script now runs the execute function automatically when launched
  10. Minor adjustments:

    • Updated variable names for clarity
    • Improved string concatenation and formatting throughout the script
  11. Compatibility:

    • Explicitly mentioned compatibility with modern MacOS and Python3

These improvements make the V4 script more robust, efficient, and user-friendly compared to the V1 version, with better handling of email threads and improved information extraction capabilities.

EmailsToCallSheet

A Drafts AppleScript action that takes a selected email thread in the macOS Mail App, processes it using the Google Gemini API to generate a project call sheet and reconstruct the conversation, and creates a new Draft containing both.

Code Repository: https://github.com/ddegner/ProjectToDraft/tree/main (Note: Repository name may differ from action name. Link assumed, update if necessary)
Detailed Write-up: https://www.daviddegner.com (Link assumed, update if necessary)

Overview

This AppleScript integrates with the macOS Mail app and the Google Gemini API. When run on a selected email thread using the “EmailsToCallSheet” action in Drafts, it sends the conversation content to Gemini twice:
1. To extract key project details (location, timeline, budget, etc.) based on a detailed prompt and format them into a Markdown call sheet.
2. To reconstruct the email thread chronologically in Markdown, removing redundant quoted text and signatures for clarity.

The resulting call sheet and the reconstructed thread are saved together in a single new draft in the Drafts app, tagged for easy organization. This script is particularly useful for professionals like photographers who need to quickly summarize client communications and project details.

Features

  • Integrates with Google Gemini API: Leverages Gemini’s language processing capabilities (configurable model).
  • Structured Call Sheet Generation: Extracts key information based on a customizable prompt and formats it into a Markdown call sheet.
  • Email Thread Reconstruction: Cleans up and chronologically reconstructs the email thread in Markdown, removing redundant content.
  • Dual Output: Combines the generated call sheet and the reconstructed thread into one Drafts note.
  • Drafts App Integration: Creates a new draft with the processed content and applies user-defined tags.
  • Secure API Key Storage: Uses macOS Keychain to store the Gemini API key securely.
  • User-Configurable: Allows easy adjustment of the API key name, Gemini model, Drafts tags, and the core prompts directly within the script.
  • Robust Error Handling: Includes checks for API key presence, Mail selection, and provides feedback on API call failures.

Dependencies

  • Google Gemini API Key: You need a Gemini API key. Store this key in your macOS Keychain. By default, the script looks for a key named Gemini_API_Key, but this name is configurable within the script. sh # Replace <username> with your macOS username and <YOUR_GEMINI_API_KEY> with your actual key # Use "Gemini_API_Key" or the custom name you set in the script's `geminiAPIKeyName` variable. security add-generic-password -a "<username>" -s "Gemini_API_Key" -w "<YOUR_GEMINI_API_KEY>"
  • macOS Mail Application: The script works with email threads selected in the macOS Mail app.
  • Python 3: Required for making API calls to the Gemini API. Python 3 is pre-installed on recent macOS versions. You can verify by opening Terminal and typing python3 --version.
  • Drafts Application: The script creates a new draft in the Drafts app (macOS version).

Installation

  1. Get a Gemini API Key: Obtain an API key from Google AI Studio (see section below).
  2. Store API Key in Keychain: Use the security command in Terminal (shown above) to store your Gemini API key securely in the macOS Keychain. Make sure the service name (-s) matches the geminiAPIKeyName variable in the script (default is “Gemini_API_Key”).
  3. Install Python 3: Ensure Python 3 is available on your system. If not, consider installing it via Homebrew (brew install python) or from the official Python website.
  4. Create Drafts Action: In the Drafts app on your Mac, create a new action. Name it “EmailsToCallSheet” (or your preferred name). Add a “Run AppleScript” step. Paste the entire AppleScript code into the script window.
  5. Configure Action Settings: Uncheck “Run in Background” if you want to see potential error dialogs. Ensure the action is available on macOS (iOS/iPadOS will not work due to Mail/Keychain dependencies).
  6. Grant Permissions: The first time you run the action, macOS will likely ask for permission to allow Drafts (or the script runner) to control the Mail app and potentially access temporary file locations. Grant these permissions.

Getting a Google Gemini API Key

  1. Visit Google AI Studio: Go to https://aistudio.google.com/.
  2. Sign In: Log in with your Google account.
  3. Get API Key: Look for an option like “Get API key” or navigate to the API key section.
  4. Create Key: Generate a new API key.
  5. Copy and Store: Copy the generated key and store it securely in your macOS Keychain using the security add-generic-password command mentioned in the Dependencies section.

Usage

  1. Select Emails: Open the macOS Mail app and select one or more messages belonging to the email thread you want to process. The script will attempt to find related messages based on the sender and subject of the first selected email.
  2. Run the Drafts Action: Trigger the “EmailsToCallSheet” action from the Drafts action list.
  3. Review Output: The script will:
    • Gather related emails and sort them chronologically.
    • Call the Gemini API twice (once for the call sheet, once for reconstruction).
    • Create a new draft in Drafts containing the Markdown call sheet followed by the reconstructed email thread, separated by a horizontal rule. The draft will have the tags specified in the draftsTags variable.

Customization

You can easily customize the script’s behavior by editing the USER-ADJUSTABLE VARIABLES section at the top of the AppleScript:

  • geminiAPIKeyName: Change the name used to retrieve the API key from Keychain if you prefer something other than "Gemini_API_Key".
  • geminiModel: Specify a different Gemini model (e.g., "gemini-1.5-flash") if desired. Ensure the model is compatible with the API endpoint used.
  • draftsTags: Modify the list of tags applied to the new draft (e.g., {"client-project", "summary"}).
  • prompt_intro: (Major Customization) Modify this text to change the structure, sections, or instructions for generating the call sheet. You can add, remove, or redefine the sections Gemini should extract.
  • conversation_prompt_intro: Adjust this prompt to change how the email thread is reconstructed (e.g., modify the formatting instructions).

Script Details

  • execute(): The main function orchestrating the process: getting emails, sorting, calling helper functions, interacting with the API, and creating the Draft.
  • sortMessagesByDate(): Sorts the collected Mail messages chronologically before processing.
  • getAPIKeyFromKeychain(): Retrieves the specified API key from the macOS Keychain.
  • writeToFile(): Writes the generated prompts to temporary files, necessary for passing potentially large text content to the shell script.
  • callGeminiAPI(): Executes an embedded Python script via do shell script to handle the actual HTTPS request to the Google Gemini API, including payload formatting and basic error handling.
  • Output Format: The final draft contains the call sheet (generated using prompt_intro) followed by ------------------------------------ and then the reconstructed conversation (generated using conversation_prompt_intro).

Security Considerations

  • API Key Security: Your Gemini API key is stored in the macOS Keychain, which is generally secure. Avoid running this script on untrusted machines.
  • Data Privacy: The content of your selected email thread is sent to Google’s Gemini API for processing. Review Google’s API data usage policies.
  • Internet Access: An active internet connection is required for the script to contact the Gemini API.

License

This script is open source and available under the MIT License. (Assumed, update if different)

Contributing

Contributions, bug reports, and feature requests are welcome. Please use the GitHub repository’s Issues or Pull Requests features. (Assumed, update if different)

Troubleshooting

  • “API Key Not Found” Error: Ensure you’ve added the Gemini API key to Keychain using the exact name specified in the geminiAPIKeyName variable (default: "Gemini_API_Key") and the correct security add-generic-password command.
  • API Errors (e.g., 4xx, 5xx): Check your internet connection. Verify the geminiModel name is correct and accessible with your API key. Check the Google Cloud Console or AI Studio for API status or billing issues. Error details might be shown in an alert dialog.
  • Python Errors: Ensure Python 3 is correctly installed and accessible via /usr/bin/python3. The script relies on standard Python libraries (json, urllib).
  • Permission Denied Errors: Make sure Drafts has permission to control Mail and potentially write temporary files. Check System Settings > Privacy & Security > Automation and Full Disk Access (though Full Disk Access might not be needed if temporary files work correctly).
  • No Emails Found/Processed: Ensure you have selected a valid email in the Mail app before running the action.

Steps

  • runAppleScript (macOS only)

    use framework "Foundation"
    use scripting additions
    
    -- *** USER-ADJUSTABLE VARIABLES ***
    property geminiAPIKeyName : "Gemini_API_Key" -- Name of the API key in Keychain
    property geminiModel : "gemini-2.5-pro-exp-03-25" -- Gemini model to use (e.g., "gemini-2.5-pro-exp-03-25", "gemini-2.0-flash")
    property draftsTags : {"callsheet"} -- Multiple tags to apply to the new draft in Drafts
    property prompt_intro : "You are a highly skilled administrative assistant. Your task is to create a markdown call sheet for photographer David Degner.  Extract all relevant project details from the following email thread with his client to populate the call sheet sections below.
    
    Formatting Instructions:
    
    Format the call sheet in markdown.
    The first line should be the shoot date and the project title in the format: # YYYYMMDD - {project-title}
    If the shoot date is unknown use XXXXXXXXX in place of the YYYYMMDD.
    Include markdown headings for each of the sections listed below.
    For sections with no information from the email thread, include only the heading and leave the content blank.
    Do not include information not explicitly stated in the email thread.
    Omit conversational pleasantries and sign-offs.
    Do NOT use HTML; use markdown for all text formatting.
    Section Headings and Information to Extract:
    
    LOCATION: Specify the photography location or client address and start time.
    
    PROJECT DESCRIPTION: Summarize the project's key objectives, scope, and any mentioned style, goals, or focus areas.
    
    TEAM AND ROLES: Identify all mentioned team members, subjects and their roles.
    
    CLIENT INFORMATION:  List the client or company name, main contact person (and their role, if mentioned), and relevant contact details (email, phone) directly, without labels. Include the agency name and contact information if an agency is involved.
    
    PROJECT TIMELINE: List and label relevant dates mentioned in the email, such as deadlines, shoot dates, and delivery timelines.
    
    DELIVERABLES: List all required outputs (photos, videos) with quantity, format, and settings.
    
    BUDGET:  Extract all mentions of budgets, costs, fees, or pricing.  Include estimates, quotes, rates, and any monetary values (e.g., '$500', 'USD', 'total cost').  Capture all financial details, even if implied or indirect. Look for keywords like 'budget', 'cost', 'estimate', 'fee', 'pricing', 'cost breakdown', 'quote', 'rate'."
    
    property conversation_prompt_intro : "Please reconstruct the following emails into a coherent email thread, presenting the messages in the correct chronological order.  Remove any redundent quoted text or redundant email signatures.  For each message, please include the sender's name, the date, and the time the message was sent, followed by the message content.  Format each message in markdown like this:
    
    **From:** Sender Name, Date of message, Time of message
    
    Message Content
    
    ---
    
    Email Thread Content:"
    
    -- *** END USER-ADJUSTABLE VARIABLES ***
    
    -- Function to replace characters in a string (Not strictly necessary, but kept for now)
    on replace_chars(theText, searchString, replacementString)
    	set AppleScript's text item delimiters to searchString
    	set theItems to text items of theText
    	set AppleScript's text item delimiters to replacementString
    	set theText to theItems as string
    	set AppleScript's text item delimiters to ""
    	return theText
    end replace_chars
    
    -- Helper function to trim whitespace from a string
    on trim(someText)
    	set nsText to current application's NSString's stringWithString:someText
    	set trimmedText to nsText's stringByTrimmingCharactersInSet:(current application's NSCharacterSet's whitespaceAndNewlineCharacterSet())
    	return trimmedText as string
    end trim
    
    -- Helper function to URL-encode a string
    on urlEncode(inputString)
    	set NSString to current application's NSString's stringWithString:inputString
    	set allowedChars to current application's NSCharacterSet's URLQueryAllowedCharacterSet()
    	set encodedString to NSString's stringByAddingPercentEncodingWithAllowedCharacters:allowedChars
    	return encodedString as string
    end urlEncode
    
    -- Function to create a message link for a given message
    on createMessageLink(theMessage)
    	tell application "Mail"
    		set messageId to message id of theMessage
    		set messageSubject to subject of theMessage
    	end tell
    	set messageLink to "message://%3c" & messageId & "%3e"
    	set markdownLink to "[" & messageSubject & "](" & messageLink & ")"
    	return markdownLink
    end createMessageLink
    
    -- Function to write text to a file using ASObjC
    on writeToFile(theText, theFilePath)
    	try
    		set theNSString to current application's NSString's stringWithString:theText
    		set theNSData to theNSString's dataUsingEncoding:(current application's NSUTF8StringEncoding)
    		theNSData's writeToFile:theFilePath atomically:true
    		return true
    	on error errMsg
    		display alert "Failed to write to file: " & errMsg
    		return false
    	end try
    end writeToFile
    
    -- Function to retrieve API key from Keychain
    on getAPIKeyFromKeychain(keyName)
    	try
    		set apiKey to do shell script "security find-generic-password -w -s " & quoted form of keyName
    		return apiKey
    	on error
    		return missing value
    	end try
    end getAPIKeyFromKeychain
    
    -- Function to sort messages by date using a custom sort
    on sortMessagesByDate(messageList)
    	set sortedMessages to messageList
    	set messageCount to count of sortedMessages
    	tell application "Mail"
    		repeat with i from 1 to (messageCount - 1)
    			repeat with j from (i + 1) to messageCount
    				set messageI to item i of sortedMessages
    				set messageJ to item j of sortedMessages
    				set dateI to date received of messageI
    				set dateJ to date received of messageJ
    				if dateI > dateJ then
    					-- Swap the messages
    					set item i of sortedMessages to messageJ
    					set item j of sortedMessages to messageI
    				end if
    			end repeat
    		end repeat
    	end tell
    	return sortedMessages
    end sortMessagesByDate
    
    -- Function to call Gemini API using Python (Corrected quotes and error handling)
    on callGeminiAPI(apiKey, promptFilePath)
    	set pythonScript to "
    import json
    import urllib.request
    import urllib.error
    import sys
    
    api_key = '" & apiKey & "'
    
    with open('" & promptFilePath & "', 'r', encoding='utf-8') as f:
        prompt = f.read()
    
    url = f'https://generativelanguage.googleapis.com/v1beta/models/" & geminiModel & ":generateContent?key={api_key}'
    
    payload = {
        \"contents\": [{
            \"parts\": [{\"text\": prompt}]
        }]
    }
    headers = {'Content-Type': 'application/json'}
    
    req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers=headers)
    
    try:
        with urllib.request.urlopen(req) as response:
            result_json = json.loads(response.read().decode('utf-8'))
            # Extract text from the response, handling potential errors
            if 'candidates' in result_json and result_json['candidates']:
                first_candidate = result_json['candidates'][0]
                if 'content' in first_candidate and 'parts' in first_candidate['content']:
                    parts = first_candidate['content']['parts']
                    if parts and parts[0]['text']:
                        print(parts[0]['text'])
                    else:
                        print(\"Error: Text content not found in API response.\", file=sys.stderr)
                        sys.exit(1)  # Exit with an error code
                else:
                    print(\"Error: 'content' or 'parts' key not found in API response candidate.\", file=sys.stderr)
                    sys.exit(1)  # Exit with an error code
            else:
                print(\"Error: 'candidates' key not found or empty in API response.\", file=sys.stderr)
                sys.exit(1)  # Exit with an error code
    
    
    except urllib.error.HTTPError as e:
        error_info = e.read().decode('utf-8')
        print(f\"API request failed (HTTP Error): {e.code} - {error_info}\", file=sys.stderr)
        sys.exit(e.code)  # Exit with the HTTP error code
    
    except Exception as e:
        print(f\"An unexpected error occurred: {e}\", file=sys.stderr)
        sys.exit(1) # Exit with a generic error code
    
    "
    	try
    		set apiResponse to do shell script "/usr/bin/python3 -c " & quoted form of pythonScript
    		set exitCode to (do shell script "echo $?") as integer -- Get the exit code
    		
    		if exitCode is not 0 then
    			display alert "API Error" message "The Gemini API request failed.  Details:
    " & apiResponse buttons {"OK"} default button "OK"
    			return "" -- Or some other error indicator
    		end if
    		return apiResponse
    		
    	on error errMsg number errNum
    		display alert "Python Script Error" message "An error occurred in the Python script:
    " & errMsg & " (Error " & errNum & ")"
    		return "" -- Or some other error indicator
    	end try
    end callGeminiAPI
    
    
    -- Function to execute the main script
    on execute()
    	try
    		tell application "Mail"
    			-- Get the related messages
    			if not (exists message viewer 1) then
    				display alert "No message viewer" message "Please open Mail and select a message." buttons {"OK"} default button "OK"
    				return
    			end if
    			
    			set theRef to (selected messages of message viewer 1)
    			if theRef is {} then
    				display alert "No email selected" message "Please select an email thread." buttons {"OK"} default button "OK"
    				return
    			end if
    			
    			set {theSender, theSubject} to {sender, subject} of first item of theRef
    			if theSubject starts with "Re: " or theSubject starts with "Réf : " then
    				set AppleScript's text item delimiters to {"Re: ", "Réf : "}
    				set theSubject to last text item of theSubject
    			end if
    			
    			set relatedMessages to messages of message viewer 1 where all headers contains theSender and all headers contains theSubject
    			
    			-- Sort the messages by date received
    			set sortedMessages to my sortMessagesByDate(relatedMessages)
    			
    			-- Initialize variables for thread content
    			set threadContent to ""
    			
    			-- Process the messages in sorted order
    			repeat with eachMessage in sortedMessages
    				-- Get email details
    				set emailSender to sender of eachMessage
    				set emailSubject to subject of eachMessage
    				set emailDate to date received of eachMessage
    				set emailBody to content of eachMessage
    				
    				-- cleanedBody is now simply emailBody (no preprocessing)
    				set cleanedBody to emailBody
    				
    				-- Create and add the message link
    				set messageLink to my createMessageLink(eachMessage)
    				
    				-- Append email details to threadContent (Corrected line break)
    				set threadContent to threadContent & "From: " & emailSender & " / Subject: " & emailSubject & " / Date: " & emailDate & linefeed & cleanedBody & linefeed & linefeed & "Message Link: " & messageLink & linefeed & "---" & linefeed & linefeed
    			end repeat
    		end tell
    		
    		-- --- Conversation Reconstruction ---
    		set fullConversationPrompt to conversation_prompt_intro & linefeed & threadContent
    		
    		-- Generate a unique temporary file path for conversation prompt
    		set conversationPromptFilePath to do shell script "mktemp /tmp/email_conversation_prompt.XXXXXX"
    		-- Write the full conversation prompt to the temporary file
    		my writeToFile(fullConversationPrompt, conversationPromptFilePath)
    		
    		-- Retrieve the Gemini API key from Keychain
    		set geminiAPIKey to my getAPIKeyFromKeychain(geminiAPIKeyName)
    		if geminiAPIKey is missing value then
    			display alert "API Key Not Found" message "Please store your Gemini API Key in the Keychain with the key name '" & geminiAPIKeyName & "'." buttons {"OK"} default button "OK"
    			return
    		end if
    		
    		-- Use Python3 to make the API request to Gemini for conversation reconstruction
    		set reconstructedConversationResponse to my callGeminiAPI(geminiAPIKey, conversationPromptFilePath)
    		if reconstructedConversationResponse starts with "API request failed:" or reconstructedConversationResponse starts with "Error:" then
    			display alert "API Error (Conversation Reconstruction)" message reconstructedConversationResponse buttons {"OK"} default button "OK"
    			return
    		end if
    		set reconstructedConversation to reconstructedConversationResponse
    		
    		
    		-- --- Information Extraction ---
    		
    		set fullPrompt to prompt_intro & linefeed & linefeed & "Email Thread Content:" & linefeed & threadContent
    		
    		-- Generate a unique temporary file path for main prompt
    		set promptFilePath to do shell script "mktemp /tmp/email_processor_prompt.XXXXXX"
    		
    		-- Write the full prompt to the temporary file
    		my writeToFile(fullPrompt, promptFilePath)
    		
    		-- Use Python3 to make the API request to Gemini for information extraction
    		set apiResponse to my callGeminiAPI(geminiAPIKey, promptFilePath) -- Re-use geminiAPIKey
    		if apiResponse starts with "API request failed:" or apiResponse starts with "Error:" then
    			display alert "API Error (Information Extraction)" message apiResponse buttons {"OK"} default button "OK"
    			return
    		end if
    		
    		-- Create a new draft in Drafts app
    		tell application "Drafts"
    			set fullContent to "" & my replace_chars(apiResponse, return, linefeed) & linefeed & linefeed & "------------------------------------
    " & reconstructedConversation
    			make new draft with properties {content:fullContent, flagged:false, tags:draftsTags} -- Use draftsTags list
    		end tell
    		
    	on error errMsg number errNum
    		display alert "An error occurred: " & errMsg & " (Error " & errNum & ")"
    	end try
    end execute
    
    -- Run the execute function
    execute()
  • configure

    draftList
    noChange
    actionList
    noChange
    actionBar
    noChange
    tagEntry
    noChange
    loadActionGroup
    loadActionBarGroup
    loadWorkspace
    Call Sheets
    linksEnabled
    noChange
    pinningEnabled
    noChange

Options

  • After Success Default
    Notification Info
    Log Level Info
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.