Give Your Mac Mail a “Brain”: Automatic Fetching, Summarizing, and Reporting

, ,

小胡小胡0009

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:

  1. Fetching email content
  2. Calling the Kouzi agent to summarize
  3. 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.

Temporary older emails were used for testing, so the content appears dated.

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:

  1. I specified the iCloud account and the newsletter folder in the script, because that’s where all my subscription emails are collected.
  2. The script retrieves only unread emails and marks them as read after successfully extracting their content.
  3. The extracted content is saved as .txt files 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.
  4. 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

  1. Read all files in the target directory: the script scans through every file in the specified folder.
  2. Filter for .txt files created today: only files created on the current day with a .txt extension are processed.
  3. 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.
  4. Poll until AI processing completes: using the chat/retrieve endpoint, the script continuously checks the processing status until it becomes completed.
  5. Fetch and organize AI results: through the chat/message/list endpoint, the script retrieves the AI’s response and formats it as HTML, accumulating everything into an allContents variable.
  6. 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.
  7. 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 run

Add 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.js

Add 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 input

Script 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);
  });
}