Action
Call Sheet
UPDATES
8 days ago
Now Gemini based and reconstructs the email thread through gemini instead of programatically.
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:
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
New helper functions:
- Added a trim function to remove leading and trailing whitespace
- Introduced a matchesPattern function for regex-like pattern matching
Enhanced email processing:
- Now includes the creation of message links for each email in the thread
- Improved formatting of email details in the threadContent
Cleanup functionality:
- Added a cleanupTempFiles function to remove temporary files created during processing
Error handling:
- Improved error handling and user feedback throughout the script
Prompt improvements:
- Updated the prompt text with more detailed instructions
- Added a requirement to include section headings even for empty sections
File handling:
- Improved temporary file naming convention (using “email_processor_” prefix)
- Better management of file paths and cleanup
Script structure:
- Reorganized the script into more clearly defined functions
- Added comments to improve code readability and maintainability
Execution flow:
- Added an execute function to encapsulate the main script logic
- The script now runs the execute function automatically when launched
Minor adjustments:
- Updated variable names for clarity
- Improved string concatenation and formatting throughout the script
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:
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
New helper functions:
- Added a trim function to remove leading and trailing whitespace
- Introduced a matchesPattern function for regex-like pattern matching
Enhanced email processing:
- Now includes the creation of message links for each email in the thread
- Improved formatting of email details in the threadContent
Cleanup functionality:
- Added a cleanupTempFiles function to remove temporary files created during processing
Error handling:
- Improved error handling and user feedback throughout the script
Prompt improvements:
- Updated the prompt text with more detailed instructions
- Added a requirement to include section headings even for empty sections
File handling:
- Improved temporary file naming convention (using “email_processor_” prefix)
- Better management of file paths and cleanup
Script structure:
- Reorganized the script into more clearly defined functions
- Added comments to improve code readability and maintainability
Execution flow:
- Added an execute function to encapsulate the main script logic
- The script now runs the execute function automatically when launched
Minor adjustments:
- Updated variable names for clarity
- Improved string concatenation and formatting throughout the script
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
- Get a Gemini API Key: Obtain an API key from Google AI Studio (see section below).
- 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 thegeminiAPIKeyName
variable in the script (default is “Gemini_API_Key”). - 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. - 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.
- 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).
- 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
- Visit Google AI Studio: Go to https://aistudio.google.com/.
- Sign In: Log in with your Google account.
- Get API Key: Look for an option like “Get API key” or navigate to the API key section.
- Create Key: Generate a new API key.
- 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
- 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.
- Run the Drafts Action: Trigger the “EmailsToCallSheet” action from the Drafts action list.
- 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 viado 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 usingconversation_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 correctsecurity 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