Action

Call Sheet

Posted by David Degner, Last update 10 days ago

UPDATES

10 days ago

Updated after getting errors in Tahoe Beta.

Now includes support for highlighting multiple message conversations.

show all updates...

10 days ago

Updated after getting errors in Tahoe Beta.

Now includes support for highlighting multiple message conversations.

4 months ago

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

7 months ago

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

10 months ago

Fixed email link finally

10 months ago

Fixed the return/linefeed error again

11 months ago

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

11 months ago

Updating the name

11 months ago

Fix text and MarkDown

11 months ago

Converted the output to Markdown

11 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

11 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.

11 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 AppleScripts action that converts email threads from photography clients into professional call sheets using Google’s Gemini AI.

Look for Update Call Sheet script to update your call sheet.

https://github.com/ddegner/EmailsToCallSheet
https://www.daviddegner.com

Overview

These AppleScripts extract key information from email threads in the macOS Mail app and use Google’s Gemini API to create and update photography call sheets. The scripts are specifically designed for photographers who need to organize client communications into structured call sheets for photo shoots. The output is saved in the Drafts app with proper markdown formatting and linked back to the original emails.

Scripts Included

NewCallSheet.scpt

Creates a new call sheet from selected email threads by:
1. Reconstructing the conversation - Cleans up email threads into chronological order
2. Extracting call sheet information - Uses AI to populate structured sections
3. Creating a new Draft - Saves the formatted call sheet with original email thread appended

UpdateCallSheet.scpt

Updates an existing call sheet with new email information by:
1. Reading current draft - Gets the existing call sheet content
2. Processing new emails - Analyzes newly selected messages
3. Merging information - Updates relevant sections while preserving existing data
4. Updating the draft - Replaces the current draft content via URL scheme

Features

  • Google Gemini AI Integration: Uses Gemini 2.5 Pro for intelligent information extraction
  • Photography-Specific Call Sheets: Structured for location shoots, team coordination, and deliverables
  • Email Thread Reconstruction: Chronologically organizes email conversations
  • Message Deduplication: Automatically removes duplicate emails based on Message-ID
  • Mail App Deep Linking: Creates clickable links back to original emails
  • Drafts Integration: Seamlessly creates and updates drafts with proper tagging
  • Thread Relationship Detection: Groups emails by normalized subject lines (removes Re:, Fwd:, etc.)

Call Sheet Sections

The generated call sheets include these structured sections:
- LOCATION: Shoot location, address, and start time
- PROJECT DESCRIPTION: Objectives, scope, style, and goals
- TEAM AND ROLES: All mentioned team members and their roles
- CLIENT INFORMATION: Contact details, agency information
- PROJECT TIMELINE: Key dates and deadlines
- DELIVERABLES: Required outputs, formats, and quantities
- BUDGET: All financial information mentioned in emails

Dependencies

  • Google Gemini API Key: Store your Gemini API key in macOS Keychain under the service name Gemini_API_Key sh security add-generic-password -a "<username>" -s "Gemini_API_Key" -w "<YOUR_GEMINI_API_KEY>"
  • macOS Mail Application: Works with email threads selected in Mail app
  • Python 3: Required for API calls (pre-installed on modern macOS)
  • Drafts Application: Must be installed and configured for automation

Installation

  1. Set Up Gemini API Key: Store your API key in macOS Keychain using the command above
  2. Create Drafts Actions:
    • Create a new action for NewCallSheet.scpt
    • Create a second action for UpdateCallSheet.scpt
    • Add “Run AppleScript” steps and paste the respective scripts
    • Disable iOS visibility (macOS only)
  3. Grant Permissions: Allow access to Mail, filesystem, and Drafts when prompted

Getting a Google Gemini API Key

  1. Visit Google AI Studio: Go to Google AI Studio
  2. Sign In: Use your Google account to sign in
  3. Create API Key: Click “Get API key” and create a new key for your project
  4. Copy and Store: Copy the key and store it in Keychain: sh security add-generic-password -a "<username>" -s "Gemini_API_Key" -w "<YOUR_GEMINI_API_KEY>"

Usage

Creating a New Call Sheet

  1. Select Email Thread: In Mail, select one or more emails from a client thread
  2. Run NewCallSheet Action: The script will:
    • Find all related emails by subject
    • Sort chronologically and remove duplicates
    • Reconstruct the conversation cleanly
    • Extract call sheet information
    • Create a new tagged draft in Drafts

Updating an Existing Call Sheet

  1. Open Existing Call Sheet: Open a call sheet draft in Drafts
  2. Select New Emails: Switch to Mail and select additional emails
  3. Run UpdateCallSheet Action: The script will:
    • Merge new email information with existing call sheet
    • Update relevant sections without losing existing data
    • Preserve markdown formatting and structure

Configuration

User Settings (NewCallSheet.scpt)

property geminiAPIKeyName : "Gemini_API_Key" -- Keychain service name
property geminiModel : "gemini-2.5-pro" -- Gemini model to use
property draftsTags : {"callsheet"} -- Tags applied to new drafts
property maxMessagesPerThread : 50 -- Limit messages per thread
property showAlerts : true -- Show error alerts

User Settings (UpdateCallSheet.scpt)

property geminiAPIKeyName : "Gemini_API_Key" -- Keychain service name  
property geminiModel : "gemini-2.5-pro-preview-03-25" -- Gemini model to use

Technical Details

  • Thread Detection: Uses normalized subjects to find related emails
  • Message Sorting: Chronological ordering by date received
  • API Integration: Uses Python subprocess for reliable HTTP requests
  • Error Handling: Clean failures with user-friendly error messages
  • Performance: Caps thread size to manage API token limits

Security Considerations

  • API Key Storage: Gemini API key stored securely in macOS Keychain
  • Temporary Files: Uses secure temporary files for API communication
  • Local Processing: No email content stored permanently outside of Drafts

License

This project is open source and available under the MIT License.

Contributing

Contributions welcome! Feel free to:
- Submit bug reports and feature requests
- Improve error handling or add new features
- Extend support for other email workflows
- Enhance the call sheet template structure

Troubleshooting

  • API Errors: Verify your Gemini API key is correctly stored in Keychain
  • No Email Selected: Ensure emails are selected in Mail message viewer
  • Permission Issues: Grant automation access to Mail, Terminal, and Drafts
  • Python Errors: Check that Python 3 is available in your PATH

For questions or support, please open an issue in the repository: https://github.com/ddegner/EmailsToCallSheet

Steps

  • runAppleScript (macOS only)

    use framework "Foundation"
    use scripting additions
    
    -- =====================================================
    -- Drafts: Mail → Call Sheet
    -- =====================================================
    -- Notes for Drafts AppleScript actions (macOS):
    --  • Drafts calls `on execute(d)` automatically. Do NOT call execute() at top level.
    --  • Always return only primitive values (e.g., text) to avoid serialization issues.
    --  • When scripting Drafts from Drafts, don't wait for replies. Wrap creates/sets in
    --    `ignoring application responses`.
    --  • Avoid long UI interactions; Drafts may time out waiting on other apps.
    
    -- *** USER SETTINGS ***
    property geminiAPIKeyName : "Gemini_API_Key" -- Keychain service name for the Gemini API key
    property geminiModel : "gemini-2.5-pro" -- Primary model
    property draftsTags : {"callsheet"}
    property maxMessagesPerThread : 50 -- Cap to limit token/latency
    property showAlerts : true -- Set false to suppress display alerts when running from 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 redundant quoted text or redundant email signatures. For each message, 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:"
    
    -- =============================
    -- Utility helpers
    -- =============================
    
    on showAlert(t, m)
    	if showAlerts then
    		display alert t message m buttons {"OK"} default button "OK"
    	end if
    end showAlert
    
    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
    
    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
    
    on normalizeSubject(s)
    	set t to s as text
    	repeat
    		if t begins with "Re: " then
    			set t to text 5 thru -1 of t
    		else if t begins with "RE: " then
    			set t to text 5 thru -1 of t
    		else if t begins with "Fwd: " then
    			set t to text 6 thru -1 of t
    		else if t begins with "FW: " then
    			set t to text 4 thru -1 of t
    		else
    			exit repeat
    		end if
    	end repeat
    	return my trim(t)
    end normalizeSubject
    
    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
    
    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
    		my showAlert("File Write Failed", errMsg)
    		return false
    	end try
    end writeToFile
    
    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
    
    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
    					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
    
    on dedupeByMessageID(messageList)
    	set resultList to {}
    	set seenIDs to {}
    	tell application "Mail"
    		repeat with m in messageList
    			set mid to message id of m
    			if seenIDs does not contain mid then
    				set end of seenIDs to mid
    				set end of resultList to m
    			end if
    		end repeat
    	end tell
    	return resultList
    end dedupeByMessageID
    
    -- =============================
    -- Gemini call via Python (stdout returns text)
    -- Uses /usr/bin/env to find python3 across typical paths.
    -- =============================
    
    on callGeminiAPI(apiKey, promptFilePath, modelName)
    	set py to "import json, sys, urllib.request, urllib.error\n" & ¬
    		"api_key = " & quoted form of apiKey & "\n" & ¬
    		"model = " & quoted form of modelName & "\n" & ¬
    		"path = " & quoted form of promptFilePath & "\n" & ¬
    		"with open(path, 'r', encoding='utf-8') as f:\n    prompt = f.read()\n" & ¬
    		"url = f'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}'\n" & ¬
    		"payload = {'contents': [{'parts': [{'text': prompt}]}]}\n" & ¬
    		"headers = {'Content-Type': 'application/json'}\n" & ¬
    		"req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers=headers)\n" & ¬
    		"try:\n" & ¬
    		"    with urllib.request.urlopen(req) as response:\n" & ¬
    		"        j = json.loads(response.read().decode('utf-8'))\n" & ¬
    		"        print(j['candidates'][0]['content']['parts'][0]['text'])\n" & ¬
    		"except urllib.error.HTTPError as e:\n" & ¬
    		"    sys.stderr.write(e.read().decode('utf-8'))\n    sys.exit(e.code)\n" & ¬
    		"except Exception as e:\n" & ¬
    		"    sys.stderr.write(str(e))\n    sys.exit(1)\n"
    	try
    		set cmd to "/usr/bin/env -i PATH=/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin python3 -c " & quoted form of py
    		set apiResponse to do shell script cmd
    		return apiResponse
    	on error errMsg number errNum
    		my showAlert("Python Script Error", "An error occurred in the Python script:\n" & errMsg & " (Error " & errNum & ")")
    		return ""
    	end try
    end callGeminiAPI
    
    -- =============================
    -- Drafts Action Entry Point
    -- =============================
    
    on execute(d)
    	try
    		set threadContent to ""
    		set allRelated to {}
    		set sel to {}
    
    		with timeout of 600 seconds
    			tell application "Mail"
    				if not (exists message viewer 1) then
    					my showAlert("No message viewer", "Open Mail and select one or more messages.")
    					return ""
    				end if
    
    				set sel to (selected messages of message viewer 1)
    				if sel is {} then
    					my showAlert("No email selected", "Please select an email (or multiple emails) in the viewer.")
    					return ""
    				end if
    
    				-- Collect related messages for each selection (subject-normalized), then dedupe by Message-ID
    				repeat with baseMsg in sel
    					set subjRaw to subject of baseMsg
    					set subjCore to my normalizeSubject(subjRaw)
    					set matches to (messages of message viewer 1 whose subject contains subjCore)
    					repeat with m in matches
    						set end of allRelated to m
    					end repeat
    				end repeat
    			end tell
    		end timeout
    
    		set allRelated to my dedupeByMessageID(allRelated)
    		set allRelated to my sortMessagesByDate(allRelated)
    
    		-- Cap to the most recent N to keep prompts manageable
    		set totalCount to (count of allRelated)
    		if totalCount > maxMessagesPerThread then
    			set startIndex to (totalCount - maxMessagesPerThread + 1)
    			set allRelated to items startIndex thru totalCount of allRelated
    		end if
    
    		-- Build plain text thread content for the LLM (chronological)
    		tell application "Mail"
    			repeat with eachMessage in allRelated
    				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 -- (fastest available body)
    				set ds to (date string of emailDate)
    				set ts to (time string of emailDate)
    				set messageLink to my createMessageLink(eachMessage)
    
    				set threadContent to threadContent & "From: " & emailSender & " / Subject: " & emailSubject & " / Date: " & ds & " " & ts & linefeed & emailBody & linefeed & linefeed & "Message Link: " & messageLink & linefeed & "---" & linefeed & linefeed
    			end repeat
    		end tell
    
    		-- 1) Reconstruct the conversation for cleaner extraction
    		set conversationPrompt to conversation_prompt_intro & linefeed & threadContent
    		set conversationPromptFilePath to do shell script "mktemp /tmp/email_conversation_prompt.XXXXXX"
    		my writeToFile(conversationPrompt, conversationPromptFilePath)
    
    		set geminiAPIKey to my getAPIKeyFromKeychain(geminiAPIKeyName)
    		if geminiAPIKey is missing value then
    			my showAlert("API Key Not Found", "Store your Gemini API Key in Keychain with the service name '" & geminiAPIKeyName & "'.")
    			return ""
    		end if
    
    		set reconstructedConversation to my callGeminiAPI(geminiAPIKey, conversationPromptFilePath, geminiModel)
    		if reconstructedConversation is "" then return ""
    
    		-- 2) Information extraction for the call sheet, using the reconstructed conversation
    		set extractionPrompt to prompt_intro & linefeed & linefeed & "Reconstructed Email Thread:" & linefeed & reconstructedConversation
    		set promptFilePath to do shell script "mktemp /tmp/email_processor_prompt.XXXXXX"
    		my writeToFile(extractionPrompt, promptFilePath)
    
    		set callSheetText to my callGeminiAPI(geminiAPIKey, promptFilePath, geminiModel)
    		if callSheetText is "" then return ""
    
    		-- Normalize line endings, compose final draft content
    		set normalizedCallSheet to my replace_chars(callSheetText, return, linefeed)
    		set fullContent to (normalizedCallSheet & linefeed & linefeed & "------------------------------------" & linefeed & reconstructedConversation) as text
    
    		-- Create the Draft without waiting for a response
    		tell application "Drafts"
    			ignoring application responses
    				make new draft with properties {content:fullContent, flagged:false, tags:draftsTags}
    			end ignoring
    		end tell
    
    		return "" -- primitive return
    
    	on error errMsg number errNum
    		my showAlert("Error", ("An error occurred: " & errMsg & " (" & errNum & ")"))
    		return ""
    	end try
    end 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.