
Give Your Mac Mail a “Brain”: Automatic Fetching, Summarizing, and Reporting
Matrix Featured Article
Matrix is the writing community of SSPAI. We advocate sharing genuine product experiences, practical knowledge, and meaningful reflections. We regularly select high-quality Matrix articles to showcase the most authentic user perspectives. The content represents the author’s personal views. SSPAI has only made minor adjustments to the title and formatting.
Although many email clients or plugins already offer per-email summarization and translation, they often lack the ability to generate a global summary of all emails received that day. So, by combining the built-in Mail app on macOS with the Kouzi agent, I created the following workflow:
“Use AppleScript to fetch email content from the Mail app, call the Kouzi agent to summarize it, and send the summarized results back to my own inbox—executed automatically every day.”
The implementation can be broken down into three steps:
- Fetching email content
- Calling the Kouzi agent to summarize
- Setting up automation for scheduled execution
The final effect: every day, you receive a neatly timed email containing the summarized content of all your unread messages, as shown below.

Fetching email content
To avoid disrupting the reading flow, I’ve placed the full AppleScript used to fetch email content at the end of the article. Here, I’ll highlight the key ideas:
- I specified the iCloud account and the newsletter folder in the script, because that’s where all my subscription emails are collected.
- The script retrieves only unread emails and marks them as read after successfully extracting their content.
- The extracted content is saved as
.txtfiles in a designated folder, using the email subject as the filename. This keeps data storage simple and decouples it from the later AI-processing steps, making debugging easier. - For each email, the script obtains its message-ID and converts it into a URI of the format
message://%3c'messageID'%3e. Clicking this URI launches Mail and opens the corresponding email in a pop-up window—very handy for jumping to the original message after reviewing the summary.
At the end of this step, we have successfully fetched the content of all unread emails in the designated folder and saved them as text files.
Using the Kouzi Agent to Summarize Email Content
I use a JavaScript script to read the files saved in the previous step and call the Kouzi API to generate summaries. Of course, you can use DeepSeek or any other provider’s API. I chose Kouzi mainly for its asynchronous query feature. The full script is also included at the end of the article.
Script Logic
- Read all files in the target directory: the script scans through every file in the specified folder.
- Filter for
.txtfiles created today: only files created on the current day with a.txtextension are processed. - Read file content and call the conversation API: for each valid file, the script reads the content and submits it to the Kouzi chat API.
- Poll until AI processing completes: using the
chat/retrieveendpoint, the script continuously checks the processing status until it becomescompleted. - Fetch and organize AI results: through the
chat/message/listendpoint, the script retrieves the AI’s response and formats it as HTML, accumulating everything into anallContentsvariable. - Send an email once all files are processed: after processing all valid files, the script uses nodemailer to send an email containing all summaries generated that day.
- Email includes unread count and AI summaries: the subject is the current date, and the body includes the number of unread emails plus all AI-generated summaries.
Kouzi Agent Setup and Invocation
Create an agent inside the Kouzi workspace and configure the agent’s prompt. In the address bar of that setup page, you’ll find the Bot_ID, which is used as a parameter when calling the API.

Kouzi API calls consist of three steps: initiating a conversation, checking the status, and retrieving the result.
The first step, initiating a conversation, is essentially submitting a processing request to Kouzi.
Here you need to provide your API Key, Bot_ID, and a custom User_ID. Kouzi will return a Chat_ID and a Conversation_ID as unique identifiers.

The second step, checking the status, is like asking Kouzi whether the request has finished processing. Only after receiving a completed status should you fetch the result; otherwise, you may end up retrieving incomplete output.
At this stage, you must provide the Chat_ID and Conversation_ID returned in the previous step.

The third step, retrieving the result, is where you obtain the final output from the agent. You again supply the Chat_ID and Conversation_ID from step one to fetch the completed response.

Configure Automation for Scheduled Execution
With the AppleScript for fetching email content and the JavaScript script for calling the Kouzi agent ready, the next step is to create an automated workflow that runs them regularly.
Open the Automator app and create a new Calendar Alarm workflow:
Add a Run AppleScript action, paste in your AppleScript code, and remember to wrap it with the execution block, like this:
on run {input, parameters}
-- AppleScript code
end runAdd a Run Shell Script action, navigate to the folder containing your script, and execute the JavaScript file:
cd /path/to/script
/opt/homebrew/bin/node scriptName.jsAdd a Show Notification action to confirm the workflow has finished running.
The Automator configuration looks like this:

After creating it, open the Calendar app and make a new event. Set it to repeat daily, and under the Alert section choose:
Custom → Open File → Other… → select your Automator workflow.
This way, it will run automatically each day.

With this, the entire setup is complete. However, be mindful of privacy and security when using it—it’s best to apply this workflow only to newsletter or public information emails, and avoid uploading private messages containing sensitive personal data.
AppleScript for Fetching Email Content
tell application "Mail"
-- Get the "newsletter" mailbox under the iCloud account
set theAccount to account "iCloud"
set theMailbox to mailbox "newsletter" of theAccount
-- Get all unread emails in the "newsletter" mailbox
set unreadMessages to (every message of theMailbox whose read status is false)
-- Iterate through all unread emails
repeat with eachMessage in unreadMessages
-- Use the email subject as part of the filename
set theSubject to subject of eachMessage
-- Get the email content and convert it to plain text
set theContent to content of eachMessage
set plainTextContent to do shell script "echo " & quoted form of theContent & " | textutil -convert txt -stdin -stdout"
-- Get the message-ID and generate a URI
set messageID to message id of eachMessage
set messageURL to "message://" & "%3c" & messageID & "%3e"
-- Create a filename by removing illegal characters
set cleanSubject to do shell script "echo " & quoted form of theSubject & " | tr -d '\"/:<>?\\|+[]{};=,'"
set fileName to (cleanSubject & ".txt")
-- Use the specified POSIX path and convert it to AppleScript path format
set savePath to POSIX file "path to save txt files"
-- Full file path
set filePath to (savePath as string) & fileName
-- Write the email content to the txt file at the specified path
try
set fileReference to open for access file filePath with write permission
write plainTextContent to fileReference starting at eof
-- Append the email URI at the end of the file
write return & "emailURL=" & messageURL to fileReference starting at eof
close access fileReference
on error errMsg
close access file filePath
display dialog "Error: " & errMsg
end try
-- Mark the email as read
set read status of eachMessage to true
end repeat
end tell
return inputScript for Calling the Kouzi Agent
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const nodemailer = require('nodemailer');
const directoryPath = 'Directory path to store text files'; // The directory path to store text files
const today = new Date().toLocaleDateString('zh-CN'); // Get system local date
let allContents = ''; // Used to store all returned contents from the query API
let fileCounter = 0; // Used for file counting
let validFileCount = 0; // Valid file count
let emailSent = false; // A flag to track whether the email has been sent
// Read files under the specified directory
fs.readdir(directoryPath, async (err, files) => {
if (err) {
console.error('Failed to read directory:', err);
return;
}
const filePromises = files.map(file => processFile(file));
await Promise.all(filePromises);
checkAllFilesProcessed();
});
async function processFile(file) {
const filePath = path.join(directoryPath, file);
try {
const stats = await fs.promises.stat(filePath);
const fileCreationDate = stats.birthtime.toLocaleDateString('zh-CN'); // Get file creation date
if (fileCreationDate === today && path.extname(file) === '.txt') {
validFileCount++; // Increase valid file count
const content = await fs.promises.readFile(filePath, 'utf8');
await callAIAPI(content, file);
}
} catch (err) {
console.error('Error while processing file:', err);
}
}
// Use async/await to optimize async handling
async function callAIAPI(content, fileName) {
const apiUrl = 'https://api.coze.cn/v3/chat';
const headers = getHeaders();
// Choose bot_id depending on content length
const botId = content.length > 32000 ? 'bot_id' : 'bot_id';
const data = {
bot_id: botId, // Dynamic bot_id
user_id: '**',
stream: false,
auto_save_history: true,
additional_messages: [
{
role: 'user',
content: content,
content_type: 'text'
}
]
};
let attempts = 0;
const maxAttempts = 5;
while (attempts < maxAttempts) {
try {
const response = await axios.post(apiUrl, data, { headers, timeout: 5000 });
console.log('API response:', response.data);
const { id, conversation_id } = response.data.data;
await new Promise(resolve => setTimeout(resolve, 1000)); // Add wait time: 1 second
await pollConversationStatus(id, conversation_id, fileName);
break;
} catch (error) {
attempts++;
console.error(`API call error (attempt ${attempts}/${maxAttempts}):`, error);
if (attempts >= maxAttempts) {
console.error('Still failed after multiple attempts');
}
}
}
}
// Extract headers function
function getHeaders() {
return {
'Authorization': 'Bearer api_key',
'Content-Type': 'application/json'
};
}
// Poll conversation detail interface
async function pollConversationStatus(chat_id, conversation_id, fileName) {
const retrieveUrl = `https://api.coze.cn/v3/chat/retrieve?chat_id=${chat_id}&conversation_id=${conversation_id}`;
const headers = getHeaders();
let pollCount = 0;
const maxPollCount = 120;
return new Promise((resolve, reject) => {
const intervalId = setInterval(async () => {
pollCount++;
if (pollCount > maxPollCount) {
clearInterval(intervalId);
reject(new Error(`Polling timeout, chat_id: ${chat_id}, conversation_id: ${conversation_id}`));
return;
}
try {
const response = await axios.get(retrieveUrl, { headers });
// Cancel printing poll logs
// console.log('Dialog detail API response:', response.data);
if (response.data.data.status === 'completed') {
clearInterval(intervalId);
await queryAIAPI(chat_id, conversation_id, fileName);
resolve();
}
} catch (error) {
console.error('Dialog detail API error:', error);
clearInterval(intervalId);
reject(error);
}
}, 1000); // Poll every second
});
}
// Call query interface
async function queryAIAPI(chat_id, conversation_id, fileName) {
const queryUrl = `https://api.coze.cn/v3/chat/message/list?chat_id=${chat_id}&conversation_id=${conversation_id}`;
const headers = getHeaders();
try {
const response = await axios.get(queryUrl, { headers });
console.log('Query API response:', response.data);
const contents = response.data.data
.filter(item => item.type === 'answer') // Filter items of type 'answer'
.map(item => item.content)
.join('\n');
// Read last line of file to get MessageID
const filePath = path.join(directoryPath, fileName);
const fileContent = await fs.promises.readFile(filePath, 'utf8');
const lastLine = fileContent.trim().split('\n').pop();
const messageId = lastLine.split('=')[1]; // Extract MessageID
fileCounter++; // Increase file count
console.log(`File count: ${fileCounter}`);
const fileNameWithoutExt = fileName.replace(/\.[^/.]+$/, ""); // Remove extension
allContents += `<h3><a href="${messageId}">${fileCounter}. ${fileNameWithoutExt}</a></h3><br>${contents.replace(/\n/g, '<br>')}<br><br>`;
} catch (error) {
console.error('Query API error:', error);
}
}
// Check if all files are processed
function checkAllFilesProcessed() {
if (fileCounter === validFileCount && !emailSent) {
emailSent = true; // Mark email as sent
sendEmail(allContents); // Call send email function
}
}
// Send email
function sendEmail(contents) {
const transporter = nodemailer.createTransport({
host: 'smtp.qq.com',
port: 465,
secure: true, // Use SSL
auth: {
user: 'Email address', // Your email address
pass: 'Email authorization code' // Your email authorization code
}
});
const mailOptions = {
from: 'Email address', // Sender email address
to: 'Email address', // Recipient email address
subject: `${today} Daily Email Summary`, // Email subject
html: `Unread email count: ${validFileCount}<br><br>${contents}` // HTML content
};
transporter.sendMail(mailOptions, (error, info) => {
if (error) {
return console.error('Failed to send email:', error);
}
console.log('Email sent:', info.response);
});
}