AI for email processing: sorting messages and drafting replies via API

A working inbox triage setup: messages over IMAP, category, priority and a draft reply as JSON, masking personal data, human review, and the cost per 1,000 emails.

6 min · · автоматизация · бизнес

People look for "AI for email processing" when a shared support or sales inbox gets dozens or hundreds of messages a day and sorting plus routine replies eat up hours. A model via API can determine a message's topic and urgency and prepare a draft reply, leaving a person to check it and hit Send. Below: a working Python script with IMAP, a prompt with a JSON answer, privacy rules, and the cost per 1,000 emails.

What to hand to the model and what not

Good fits:

  • classification into predefined categories: order, payment, refund, complaint, partnership, spam;
  • priority — so payment problems don't wait in the same queue as newsletters;
  • a one-line summary for the task list;
  • a draft reply to routine questions.

Leave to a person: sending emails, any promises about money or deadlines, replies to complaints, and anything the model itself flagged as uncertain. Sending automatically without review is a bad idea: one mistake in a reply to a customer costs more than the time saved.

AI for email processing: the setup

  1. A script fetches unread messages from the mailbox over IMAP.
  2. It extracts the subject and text from each, masking emails and phone numbers.
  3. It sends the text to the model with instructions and gets JSON back: category, priority, whether a person is needed, summary, draft.
  4. Code validates the JSON: the category must be from the list, otherwise the message goes to a person.
  5. The result lands in a review queue — a file, a table or drafts in the mailbox.

Step 1. Fetching mail over IMAP in Python

Python's standard library has the imaplib module. IMAP4_SSL connects to the server over an encrypted connection (port 993 by default) and supports with: LOGOUT is sent automatically when the block exits. select(..., readonly=True) opens the folder read-only, so the script doesn't change message flags. search(None, "UNSEEN") finds unread messages, fetch(num, "(RFC822)") downloads a whole message.

The email module parses messages: message_from_bytes(..., policy=policy.default) returns an object whose get_body(preferencelist=("plain", "html")) finds the main text part and get_content() returns it as a string.

Many mail services require a separate app password for IMAP and IMAP access enabled in the mailbox settings — check your provider's help pages. Keep the login and password in environment variables, not in code.

Step 2. The prompt and the JSON answer

The full script: fetches up to 50 unread messages, classifies them and saves the queue to triage.json.

bash
import email
import imaplib
import json
import os
import re
from email import policy

from openai import OpenAI

CATEGORIES = ["order", "payment", "refund", "complaint", "partnership", "spam", "other"]

SYSTEM = (
    "You are an online store support assistant triaging incoming email. "
    "Pick the message category — one of: " + ", ".join(CATEGORIES) + ". "

Details worth keeping as you extend it:

  • A closed list of categories in the prompt and a check in code. If the model returns something outside the list or broken JSON, the message goes to a person instead of getting lost.
  • text[:4000] trims long messages that quote the whole thread: you don't need to pay for that, and the beginning is enough for classification.
  • max_tokens=600 caps the answer length and therefore the cost.
  • deepseek-v4.1-flash is used here as the cheapest starting point. If the drafts don't satisfy you, compare other models on 50–100 of your own messages with reference labels. If the JSON answer breaks often, use a tool call with a schema — covered in the article on strict JSON responses.

Step 3. A human in the loop

The script sends nothing. Next options:

  • A table for the operator. Load triage.json into any internal panel or spreadsheet: sort by priority, filter by needs_human, draft next to the message.
  • Drafts in the mailbox. IMAP lets you place a message into a folder with the APPEND command — in imaplib that's append(mailbox, flags, date_time, message). The operator then opens the draft in a regular mail client, edits and sends it:
bash
import imaplib
import time
from email.message import EmailMessage


def save_draft(imap, original, draft_text, folder="Drafts"):
    reply = EmailMessage()
    reply["To"] = str(original["Reply-To"] or original["From"])
    reply["Subject"] = "Re: " + str(original["Subject"] or "")
    if original["Message-ID"]:
        reply["In-Reply-To"] = str(original["Message-ID"])
    reply.set_content(draft_text)
    imap.append(folder, r"\Draft", imaplib.Time2Internaldate(time.time()), reply.as_bytes())

The drafts folder name differs between providers — list folders with imap.list(). To write, open the folder without readonly. The draft is based on masked text, so it may contain [email] and [phone] instead of real addresses and numbers — the operator fills them in during review.

Privacy

Emails are customers' personal data. Minimum rules:

  • Mask before sending. In the example, emails and phone numbers are replaced with placeholders via regular expressions. Add your own patterns: contract numbers, cards, addresses.
  • Don't log full message bodies and model answers. More in the article LLM logging without data leaks.
  • A separate key for this task with a daily limit and allowed_models permitting only the model you need. Then a bug in a loop or a leaked key won't drain the balance.
  • Check company rules and personal data law (in Russia, Federal Law 152-FZ) before sending correspondence to an external service.

What processing 1,000 emails costs

Prices as of October 7, 2026, USD per 1M tokens. Say the system prompt is 600 tokens and a trimmed message averages 900, so 1,500 input tokens in total; the answer with a draft is 350 tokens. Formula: 1,000 × (1,500 × input price + 350 × output price) / 1,000,000.

ModelInput / output1,000 emails
deepseek-v4.1-flash0.013 / 0.0381,500 × 0.013 + 350 × 0.038 = 32.8 → $0.033
gemini-3.8-flash0.063 / 0.28894.5 + 100.8 = 195.3 → $0.195
gpt-6.1-sol0.27 / 1.32405 + 462 = 867 → $0.867
claude-sonnet-50.4 / 2600 + 700 = 1,300 → $1.30

Caching does almost nothing here: a 600-token prompt is shorter than the minimum cacheable prefix — per Anthropic's docs that's 1,024 tokens for Claude Sonnet 5, and per OpenAI's docs the same for GPT-5.6 and later. If your instructions with sample replies are longer, caching kicks in and you should price with the catalog's cache-read rates. For Claude the cache discount applies only in the Anthropic format: in the OpenAI format, as in the script above, all Claude input is billed at the input price. More in the key limits and cache docs. For the current list and prices, call GET https://api.zukko.pro/v1/models or open the Models page in the dashboard.

To try it on your own inbox, create a separate key with a limit on the API keys page and run the script against a copy of the mailbox or a test folder.

FAQ

Can replies be sent automatically?

Technically yes, but we don't recommend starting there. Work in draft mode first, see how often the operator edits the text, and only then decide for the most routine categories whether automation is needed.

Will the script mark messages as read?

No. The folder is opened with readonly=True, so message flags don't change. To avoid processing a message twice, store the processed Message-ID values.

Which model should write the drafts?

Start with a cheap one and compare it with one or two stronger models on 50–100 of your own messages: assess category accuracy and how many edits the drafts need. The catalog's intelligence index is a guide to strength, but your test decides; how to compare blind is covered in the article AI for writing texts.

Does this work with Gmail, Yandex Mail and corporate Exchange?

The script uses standard IMAP, so any mailbox with IMAP access enabled will do. How to enable it and whether you need an app password is in your mail service's help pages.

zukko

Pay 25×
less

for Claude Code & ChatGPT

One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.

Get API keyDocumentation

api.zukko.pro · fetch_*

AI for email processing: sorting mail via API · Zukko