🤖 A bot that reads your mail once an hour · บอทที่อ่านเมลของคุณชั่วโมงละครั้ง · 一个每小时读一次你邮件的机器人

It answers anything with horoscope in the subject, and ignores everything else. Five files. The interesting part is not the reading. It is making sure it never replies to itself. · มันตอบทุกฉบับที่หัวข้อมีคำว่า horoscope และปล่อยอย่างอื่นไว้ ห้าไฟล์ ส่วนที่น่าสนใจไม่ใช่การอ่าน แต่คือการทำให้แน่ใจว่ามันจะไม่ตอบกลับตัวเอง · 标题里有 horoscope 的它都回,其他一概不管。五个文件。有意思的不是读信,是确保它永远不会回自己的信。

🇬🇧 English

A bot that looks at your mail once an hour. If the subject says horoscope, it writes one and replies. Everything else it leaves alone.

The hard part is not the reading. It is not replying to itself. A bot that answers its own answer will fill your inbox by morning.

So it puts a mark on everything it sends, and looks for that mark before it answers anything. Seven files, and it uses the email code you already wrote.

🇹🇭 ไทย

บอทที่เปิดดูเมลของคุณชั่วโมงละครั้ง ถ้าหัวข้อมีคำว่า horoscope มันจะเขียนดวงประจำวันแล้วตอบกลับ นอกนั้นปล่อยไว้เฉย ๆ

ส่วนที่ยากไม่ใช่การอ่าน แต่คือการไม่ตอบกลับตัวเอง บอทที่ตอบคำตอบของตัวเองจะทำให้กล่องจดหมายคุณเต็มภายในคืนเดียว

มันจึงติดเครื่องหมายไว้ในทุกฉบับที่ส่งออกไป แล้วมองหาเครื่องหมายนั้นก่อนจะตอบอะไรก็ตาม เจ็ดไฟล์ และใช้โค้ดอีเมลที่คุณเขียนไว้แล้ว

🇨🇳 中文

一个每小时看一次你邮件的机器人。如果标题里有 horoscope,它就写一份当天的星座运势回过去。其他的一概不管。

难的不是读信,是别回自己的信。一个会回自己回复的机器人,到早上就能把你的收件箱塞满。

所以它在寄出的每一封信上都做记号,并且在回任何信之前先找那个记号。七个文件,而且用的是你已经写过的邮件代码。

The bot seesบอทเห็น机器人看到It doesมันทำ它就
Subject has horoscopeหัวข้อมีคำว่า horoscope标题里有 horoscopereplies with today's horoscopeตอบกลับด้วยดวงของวันนี้回今天的运势
Anything elseอย่างอื่นทั้งหมด其他任何东西nothing at allไม่ทำอะไรเลย什么都不做
Its own mark on the letterเครื่องหมายของมันเองอยู่ในจดหมาย信上有它自己的记号nothing, and marks it readไม่ทำอะไร แล้วทำเครื่องหมายว่าอ่านแล้ว什么都不做,标成已读
RUN THIS ON A MAILBOX YOU OWN Two robots answering each other is a real thing. It is called a mail loop. Say this bot replies to somebody's holiday autoresponder. The two will write to each other until a provider stops them. Keep it on your own mailbox until the skipping works. หุ่นยนต์สองตัวตอบกันไปมาเป็นเรื่องที่เกิดขึ้นจริง เรียกว่า mail loop ถ้าบอทนี้ไปตอบข้อความตอบกลับอัตโนมัติช่วงวันหยุดของใครสักคน ทั้งคู่จะเขียนหากันไปเรื่อย ๆ จนกว่าผู้ให้บริการอีเมลจะเข้ามาหยุด ให้ใช้กับกล่องจดหมายของคุณเองจนกว่าการข้ามจะทำงานถูกต้อง 两个机器人互相回信是真实存在的事,叫做邮件循环。如果这个机器人回了某人的休假自动回复,它们两个会一直写下去,直到某个邮件服务商出手拦住。在跳过逻辑没跑通之前,只在你自己的邮箱上用它。

👀 Tutorial 1 — Look before you touch · บทที่ 1 — ดูก่อน อย่าเพิ่งแตะ · 教程 1 —— 先看,别动

🇬🇧 English

The first version sends nothing and changes nothing. It opens the mailbox read only, and says out loud what it would have done.

Run a bot that can write before you have watched one that cannot, and you will find out what it does by reading your sent folder.

🇹🇭 ไทย

เวอร์ชันแรกไม่ส่งอะไรและไม่เปลี่ยนอะไรเลย มันเปิดกล่องจดหมายแบบอ่านอย่างเดียว แล้วพูดออกมาว่ามันจะทำอะไร

ถ้ารันบอทที่เขียนได้ก่อนจะเคยดูบอทที่เขียนไม่ได้ คุณจะได้รู้ว่ามันทำอะไรจากการไปไล่อ่านโฟลเดอร์ส่งแล้ว

🇨🇳 中文

第一个版本什么都不发,什么都不改。它以只读方式打开邮箱,然后把它本来会做的事说出来。

还没看过一个不会写信的机器人,就先跑一个会写信的,那你就会从已发送文件夹里学到它都干了什么。

1 See one email out of thousands · เห็นเมลฉบับเดียวจากพัน ๆ ฉบับ · 从几千封里看见那一封

🇬🇧 English

The mailbox used to write this page had 2,249 unread emails. The bot must not download 2,249 emails to find one.

So the search happens on the server. UNSEEN SUBJECT "horoscope" asks the mail provider to do the sorting, and it hands back one number.

🇹🇭 ไทย

กล่องจดหมายที่ใช้เขียนหน้านี้มีเมลที่ยังไม่ได้อ่าน 2,249 ฉบับ บอทต้องไม่ดาวน์โหลดทั้ง 2,249 ฉบับเพื่อหาฉบับเดียว

การค้นหาจึงเกิดขึ้นบนเซิร์ฟเวอร์ UNSEEN SUBJECT "horoscope" คือการบอกผู้ให้บริการอีเมลให้คัดให้ แล้วมันส่งกลับมาแค่ตัวเลขเดียว

🇨🇳 中文

写这一页用的那个邮箱有 2,249 封未读。机器人不能为了找一封就下载 2,249 封。

所以搜索发生在服务器上。UNSEEN SUBJECT "horoscope" 是让邮件服务商替你筛,它只递回一个数字。

📄 NEW FILE · bot1.py

"""bot1.py — look at the mail, decide nothing, change nothing."""
import email
import email.policy
import imaplib
import os
import sys

IMAP_HOST = "imap.gmail.com"      # yahoo: imap.mail.yahoo.com
WANTED = "horoscope"
TAG = "[horoscope-bot]"    # what the bot will put on its own letters, in step 3

sys.stdout.reconfigure(encoding="utf-8")


def main():
    """Ask the server for the interesting mail, and say what we would do."""
    box = imaplib.IMAP4_SSL(IMAP_HOST)
    box.login(os.environ["MAIL_USER"], os.environ["MAIL_PASS"])

    # readonly, so nothing can be marked as read by accident today.
    box.select("INBOX", readonly=True)

    # Let the server do the filtering. This mailbox has thousands of unread
    # emails, and none of them are the bot's business except these.
    status, ids = box.search(None, "UNSEEN", "SUBJECT", '"' + WANTED + '"')
    wanted = ids[0].split()
    print("unread emails asking for a", WANTED + ":", len(wanted))

    for message_id in wanted:
        # Only the headers. The subject is all we need, so do not download
        # the rest of the letter.
        status, raw = box.fetch(message_id, "(BODY.PEEK[HEADER])")
        message = email.message_from_bytes(raw[0][1], policy=email.policy.default)
        subject = message["Subject"] or ""

        print()
        print("from   :", message["From"])
        print("subject:", subject)
        if TAG in subject:
            print("decide : leave it. This one is already ours.")
        else:
            print("decide : send a horoscope.")

    box.logout()


if __name__ == "__main__":
    main()
LineWhat it does · ทำอะไร · 做什么
box.select("INBOX", readonly=True)Read only. Nothing can be marked as read by accident today.อ่านอย่างเดียว วันนี้จะไม่มีอะไรถูกทำเครื่องหมายว่าอ่านแล้วโดยบังเอิญ只读。今天不会有东西被不小心标成已读。
box.search(None, "UNSEEN", "SUBJECT", '"' + WANTED + '"')Two conditions, both on the server. It answers with ids, not emails.สองเงื่อนไข ทำบนเซิร์ฟเวอร์ทั้งคู่ มันตอบกลับมาเป็นเลขที่ ไม่ใช่ตัวเมล两个条件,都在服务器上跑。它回的是编号,不是邮件。
box.fetch(message_id, "(BODY.PEEK[HEADER])")Headers only. The subject decides, so leave the letter itself alone.เอาแต่ส่วนหัว หัวข้อคือสิ่งที่ตัดสิน จึงไม่ต้องไปยุ่งกับตัวจดหมาย只要信头。标题就能决定,所以别去动信的内容。
if TAG in subject:The whole decision, in one line. Everything else is saying it out loud.การตัดสินใจทั้งหมดอยู่ในบรรทัดเดียว ที่เหลือคือการพูดมันออกมา整个判断就这一行。其余的只是把它说出来。

🖥️ WHAT IT PRINTED · against a real mailbox

unread emails asking for a horoscope: 1

from   : you@yahoo.com
subject: horoscope please
decide : send a horoscope.
NOT EVERY SERVER SEARCHES EVERYTHING Searching is the server's job, and servers disagree about it. Yahoo refuses a HEADER search outright. It answers: the given search command is not supported right now. Subject, date and flags are safe everywhere. That is why this bot only asks about the subject. การค้นหาเป็นงานของเซิร์ฟเวอร์ และเซิร์ฟเวอร์แต่ละเจ้าก็ไม่เหมือนกัน Yahoo ปฏิเสธการค้นแบบ HEADER ตรง ๆ โดยตอบว่า the given search command is not supported right now ส่วนหัวข้อ วันที่ และสถานะ ใช้ได้ทุกที่ นั่นคือเหตุผลที่บอทตัวนี้ถามถึงแค่หัวข้อ 搜索是服务器的活,而各家服务器的说法并不一致。Yahoo 直接拒绝 HEADER 搜索:the given search command is not supported right now。标题、日期和标记到处都能用。所以这个机器人只问标题。
YOUR TURN
  1. Send yourself an email with horoscope in the subject.
  2. Run it. Check: it finds one, and decides to reply.
  3. Send another with a different subject. It should not appear.
  4. Count the unread in your own mailbox. How many did it skip?
  1. ส่งเมลหาตัวเองโดยให้หัวข้อมีคำว่า horoscope
  2. รันดู ตรวจ: ต้องเจอหนึ่งฉบับ และตัดสินใจว่าจะตอบ
  3. ส่งอีกฉบับด้วยหัวข้ออื่น มันต้องไม่โผล่มา
  4. นับเมลที่ยังไม่ได้อ่านในกล่องของคุณ มันข้ามไปกี่ฉบับ
  1. 给自己发一封标题里带 horoscope 的邮件。
  2. 运行。检查:它找到一封,并决定要回。
  3. 再发一封别的标题的,它不该出现。
  4. 数数你邮箱里的未读。它跳过了多少封?

🔮 Tutorial 2 — Something to say · บทที่ 2 — มีอะไรจะพูด · 教程 2 —— 得有话可说

🇬🇧 English

The horoscope comes from the model on your own machine. Asking it every time would give a different answer every time.

A newspaper does not do that. Two people asking an hour apart should read the same thing, so it is written once a day and kept.

🇹🇭 ไทย

ดวงประจำวันมาจากโมเดลบนเครื่องของคุณเอง ถ้าถามมันทุกครั้ง ก็จะได้คำตอบใหม่ทุกครั้ง

หนังสือพิมพ์ไม่ทำแบบนั้น คนสองคนที่ถามห่างกันหนึ่งชั่วโมงควรได้อ่านเรื่องเดียวกัน มันจึงถูกเขียนวันละครั้งแล้วเก็บไว้

🇨🇳 中文

运势来自你自己机器上的模型。每次都问,就每次都不一样。

报纸不是这样的。相隔一小时问的两个人应该读到同一份,所以它一天只写一次,然后存起来。

2 One horoscope a day, written once · วันละหนึ่งดวง เขียนครั้งเดียว · 一天一份,只写一次

🇬🇧 English

The file is a dictionary from day to horoscope. If today is already in it, the model is not asked at all.

That is a cache, and it is the first one most programs need. It also means the bot answers instantly for the rest of the day.

🇹🇭 ไทย

ไฟล์นี้คือดิกชันนารีที่แมปวันไปหาดวง ถ้าวันนี้มีอยู่แล้ว ก็ไม่ต้องไปถามโมเดลเลย

นั่นคือแคช และเป็นแคชแรกที่โปรแกรมส่วนใหญ่ต้องการ มันยังทำให้บอทตอบได้ทันทีตลอดทั้งวันที่เหลือด้วย

🇨🇳 中文

这个文件是一个从日期映射到运势的字典。今天要是已经在里面,就完全不用去问模型。

那就是缓存,也是大多数程序最先需要的那一个。它还让机器人在这一天剩下的时间里都能立刻回答。

📄 NEW FILE · horoscope.py

"""horoscope.py — one horoscope per day, written once and kept."""
import json
import sys
import time
import urllib.request
from pathlib import Path

OLLAMA = "http://localhost:11434/api/chat"
MODEL = "llama3.2"
SAVED = Path("horoscopes.json")

sys.stdout.reconfigure(encoding="utf-8")

ASK = """Write a horoscope for everyone for today, {day}.

Four sentences. Warm, a little funny, and not spooky. Mention one small
ordinary thing that might happen. Do not mention star signs. Do not promise
money, love or health. Answer with the horoscope only."""


def ask_ollama(prompt):
    """One call to Ollama on this machine."""
    payload = json.dumps({
        "model": MODEL,
        "stream": False,
        "options": {"temperature": 0.9},
        "messages": [{"role": "user", "content": prompt}],
    }).encode()
    request = urllib.request.Request(
        OLLAMA, data=payload, headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(request, timeout=900) as response:
        return json.load(response)["message"]["content"].strip()


def load_saved():
    """Everything written so far, as a dictionary of day to horoscope."""
    if not SAVED.exists():
        return {}
    try:
        return json.loads(SAVED.read_text(encoding="utf-8"))
    except ValueError:
        # A half-written file should not stop the bot answering today.
        return {}


def for_today():
    """Today's horoscope. Written once, then read from the file all day.

    Two people asking an hour apart should get the same horoscope, the way
    a newspaper works. Asking the model twice would give two different ones.
    """
    day = time.strftime("%Y-%m-%d")
    saved = load_saved()

    if day in saved:
        return day, saved[day], "from the file"

    written = ask_ollama(ASK.format(day=day))
    saved[day] = written
    SAVED.write_text(json.dumps(saved, ensure_ascii=False, indent=2),
                     encoding="utf-8")
    return day, written, "written just now"


def main():
    day, words, where = for_today()
    print(day, "(" + where + ")")
    print()
    print(words)


if __name__ == "__main__":
    main()
LineWhat it does · ทำอะไร · 做什么
day = time.strftime("%Y-%m-%d")The date is the key. At midnight the key changes and a new one is written.วันที่คือคีย์ พอเที่ยงคืนคีย์เปลี่ยน ก็จะมีการเขียนอันใหม่日期就是键。到了午夜键变了,就会写一份新的。
if day in saved:The whole cache. Found it, hand it back, and never call the model.แคชทั้งหมดอยู่ตรงนี้ เจอแล้วก็ส่งคืน ไม่ต้องเรียกโมเดลเลย整个缓存就这么回事。找到了就交回去,根本不碰模型。
except ValueError:A half written file must not stop the bot answering today.ไฟล์ที่เขียนค้างต้องไม่ทำให้บอทตอบวันนี้ไม่ได้写了一半的文件,不能让机器人今天答不了话。

🖥️ WHAT IT PRINTED · run twice in a row

2026-09-23 (written just now)

Today, you may find yourself indulging in a sweet surprise - a
perfectly toasted slice of bread at your favorite breakfast spot.
... Expect a moment of quiet contentment, where the world feels
just right.

$ python horoscope.py
2026-09-23 (from the file)
YOUR TURN
  1. Run it. Then run it again and watch it stop asking the model.
  2. Open horoscopes.json. One key, one horoscope.
  3. Change the date in the file by hand and run it again.
  4. Change ASK so it writes for one star sign instead.
  1. รันดู แล้วรันอีกครั้ง สังเกตว่ามันเลิกถามโมเดลแล้ว
  2. เปิด horoscopes.json มีหนึ่งคีย์ หนึ่งดวง
  3. ลองแก้วันที่ในไฟล์ด้วยมือ แล้วรันใหม่
  4. แก้ ASK ให้มันเขียนสำหรับราศีเดียวแทน
  1. 运行一次,再运行一次,看它不再去问模型。
  2. 打开 horoscopes.json:一个键,一份运势。
  3. 手动改一下文件里的日期,再运行一次。
  4. 改 ASK,让它只写一个星座的。

🔏 Tutorial 3 — One mark, in the subject · บทที่ 3 — เครื่องหมายเดียว ในหัวข้อ · 教程 3 —— 一个记号,放在标题里

🇬🇧 English

Reply to a horoscope and the subject still says horoscope. The bot finds it again and answers again, all night.

The fix is one word in the subject line, and one line of Python to look for it.

🇹🇭 ไทย

ตอบกลับดวงประจำวัน หัวข้อก็ยังมีคำว่า horoscope อยู่ดี บอทจะเจอมันอีก แล้วตอบอีก ทั้งคืน

วิธีแก้คือคำเดียวในบรรทัดหัวข้อ และโค้ด Python บรรทัดเดียวที่คอยมองหามัน

🇨🇳 中文

回复一封运势邮件,标题里照样有 horoscope。机器人会再找到它、再回一次,回一整晚。

解决办法是标题里的一个词,和一行去找它的 Python。

3 The tag comes home on its own · แท็กกลับบ้านเอง · 标签会自己回家

🇬🇧 English

Every letter the bot sends gets [horoscope-bot] on the end of the subject.

Then the useful part. Anything that answers a letter quotes its subject back. A reply does. A forward does. Even an out of office does.

So the tag comes back by itself, and looking at the subject is the whole test.

🇹🇭 ไทย

ทุกฉบับที่บอทส่งจะมี [horoscope-bot] ต่อท้ายหัวข้อ

แล้วส่วนที่มีประโยชน์ก็คือ อะไรก็ตามที่ตอบจดหมายจะยกหัวข้อนั้นกลับมาด้วย การตอบกลับก็ยก การฟอร์เวิร์ดก็ยก แม้แต่ข้อความไม่อยู่ออฟฟิศก็ยก

แท็กจึงกลับมาเอง และการดูหัวข้อก็คือการตรวจทั้งหมด

🇨🇳 中文

机器人寄出的每封信,标题末尾都会加上 [horoscope-bot]。

然后是有用的那部分:任何回应一封信的东西,都会把它的标题引回来。回复会,转发会,连外出自动回复也会。

所以标签自己就回来了,看标题就是全部的检查。

📄 NEW FILE · mark.py

"""mark.py — one mark, in the subject, where every reply carries it along."""
import sys
from email.message import EmailMessage

TAG = "[horoscope-bot]"

sys.stdout.reconfigure(encoding="utf-8")


def tagged_subject(original):
    """Put Re: on the front and the tag on the end."""
    subject = original or "horoscope"
    if not subject.lower().startswith("re:"):
        subject = "Re: " + subject
    if TAG in subject:
        return subject
    return subject + " " + TAG


def build_reply(sender, to, subject, horoscope):
    """One horoscope letter, with the tag in the subject."""
    message = EmailMessage()
    message["From"] = sender
    message["To"] = to
    message["Subject"] = tagged_subject(subject)

    # Good manners. It tells other robots not to answer this letter.
    message["Auto-Submitted"] = "auto-replied"

    message.set_content(horoscope + "\n\n--\nwritten by a robot, once a day\n")
    return message


def ours_already(message):
    """Have we already answered this conversation?

    Anything that answers a letter quotes its subject back. A reply, a
    forward, even an out of office. So the tag comes home on its own, and
    looking at the subject is the whole test.
    """
    return TAG in (message.get("Subject") or "")


def main():
    """Show the tag going out, and surviving the three ways back."""
    letter = build_reply("bot@example.com", "you@example.com",
                         "horoscope please", "Today will be fine.")
    went_out = letter["Subject"]
    print("we send:", went_out)
    print()

    coming_back = [
        ("a reply", "Re: " + went_out),
        ("a forward", "Fwd: " + went_out),
        ("an out of office", "Out of office: " + went_out),
        ("a new question", "horoscope for tomorrow"),
    ]
    for how, subject in coming_back:
        check = EmailMessage()
        check["Subject"] = subject
        print(how.ljust(17), "->", "skip" if ours_already(check) else "answer it")


if __name__ == "__main__":
    main()
LineWhat it does · ทำอะไร · 做什么
return subject + " " + TAGThe mark goes out. Visible, on purpose, so a reader knows too.เครื่องหมายออกไปพร้อมจดหมาย มองเห็นได้โดยตั้งใจ คนอ่านจะได้รู้ด้วย记号跟着信出去。故意让它看得见,好让读的人也知道。
return TAG in (message.get("Subject") or "")The mark comes back. One line, and it is the whole decision.เครื่องหมายกลับมา บรรทัดเดียว และนั่นคือการตัดสินใจทั้งหมด记号回来了。一行,就是全部的判断。
message["Auto-Submitted"] = "auto-replied"Good manners. It asks other robots not to answer this letter.มารยาทที่ดี มันบอกหุ่นยนต์ตัวอื่นว่าอย่าตอบจดหมายฉบับนี้礼貌。它请别的机器人别回这封信。

🖥️ WHAT IT PRINTED · the three ways back, and one new question

we send: Re: horoscope please [horoscope-bot]

a reply           -> skip
a forward         -> skip
an out of office  -> skip
a new question    -> answer it
DO NOT HIDE THE MARK An earlier draft of this page hid it. White text on a white background. Two things were wrong. SpamAssassin scores hidden text. The bot's own mail starts heading for the spam folder. And somebody replying deserves to know they are writing to a program. The visible tag is simpler and kinder. หน้านี้ฉบับร่างก่อนเคยซ่อนมันไว้ เป็นข้อความสีขาวบนพื้นขาว มีสองอย่างที่ผิด SpamAssassin ให้คะแนนข้อความที่ซ่อนไว้ เมลของบอทเองจึงเริ่มมุ่งหน้าไปโฟลเดอร์สแปม และคนที่ตอบกลับมาก็ควรได้รู้ว่ากำลังเขียนหาโปรแกรม แท็กที่มองเห็นได้นั้นง่ายกว่าและใจดีกว่า 这一页早先的草稿把它藏了起来,白底白字。有两点不对。SpamAssassin 会给隐藏文字打分,机器人自己的信就开始往垃圾箱走。而且回信的人有权知道自己在跟一个程序说话。看得见的标签更简单,也更厚道。
YOUR TURN
  1. Run it, and read the four answers it gives.
  2. Send yourself one. Check: the tag is in the subject.
  3. Reply to it. Check: your reply still carries the tag.
  4. Change TAG to your own word and run it again.
  1. รันดู แล้วอ่านคำตอบสี่อันที่มันให้
  2. ส่งหาตัวเองหนึ่งฉบับ ตรวจ: แท็กต้องอยู่ในหัวข้อ
  3. ตอบกลับมันดู ตรวจ: คำตอบของคุณยังพกแท็กมาด้วย
  4. เปลี่ยน TAG เป็นคำของคุณเอง แล้วรันใหม่
  1. 运行它,读一读它给的四个答案。
  2. 给自己发一封。检查:标题里有那个标签。
  3. 回复它。检查:你的回复照样带着标签。
  4. 把 TAG 换成你自己的词,再运行一次。

📬 Tutorial 4 — Answer, and only once · บทที่ 4 — ตอบ และตอบแค่ครั้งเดียว · 教程 4 —— 回,而且只回一次

🇬🇧 English

Now it writes. One pass first, watched, by hand. Then the same pass on a timer.

🇹🇭 ไทย

คราวนี้มันเขียนได้แล้ว เริ่มจากรอบเดียวก่อน โดยมีคนดูอยู่ แล้วค่อยเอารอบเดิมนั้นไปตั้งเวลา

🇨🇳 中文

现在它会写信了。先手动跑一轮,看着它。然后把同一轮挂到定时器上。

4 One pass, watched · หนึ่งรอบ โดยมีคนดู · 手动跑一轮

🇬🇧 English

Read, decide, reply, mark as read. The deciding is one call to ours_already, and everything hangs on it.

The reply carries In-Reply-To so it lands in the same conversation. That header is also what the next pass will recognise.

🇹🇭 ไทย

อ่าน ตัดสินใจ ตอบ แล้วทำเครื่องหมายว่าอ่านแล้ว การตัดสินใจคือการเรียก ours_already ครั้งเดียว และทุกอย่างขึ้นอยู่กับมัน

คำตอบจะพก In-Reply-To ไปด้วย เพื่อให้ไปอยู่ในบทสนทนาเดียวกัน และเฮดเดอร์นั้นเองคือสิ่งที่รอบถัดไปจะจำได้

🇨🇳 中文

读、判断、回、标成已读。判断就是调用一次 ours_already,一切都押在它身上。

回复会带上 In-Reply-To,好落在同一个会话里。而下一轮认出它的,也正是这个信头。

📄 NEW FILE · bot2.py

"""bot2.py — one pass. Read, decide, reply, and never reply twice."""
import email
import email.policy
import email.utils
import imaplib
import os
import smtplib
import sys

from horoscope import for_today
from mark import build_reply, ours_already

IMAP_HOST = "imap.gmail.com"      # yahoo: imap.mail.yahoo.com
SMTP_HOST = "smtp.gmail.com"      # yahoo: smtp.mail.yahoo.com
WANTED = "horoscope"

sys.stdout.reconfigure(encoding="utf-8")


def one_pass(user, password):
    """Look once. Answer what deserves answering. Return how many we sent."""
    box = imaplib.IMAP4_SSL(IMAP_HOST)
    box.login(user, password)

    # Not readonly this time. Anything we handle gets marked as read, so the
    # next pass does not see it again.
    box.select("INBOX")

    status, ids = box.search(None, "UNSEEN", "SUBJECT", '"' + WANTED + '"')
    waiting = ids[0].split()
    print("asking for a", WANTED + ":", len(waiting))

    sent = 0
    for message_id in waiting:
        # Headers only, same as step 1. The subject decides, and the address
        # says where the answer goes. Neither of them is in the body.
        status, raw = box.fetch(message_id, "(BODY.PEEK[HEADER])")
        message = email.message_from_bytes(raw[0][1], policy=email.policy.default)

        if ours_already(message):
            print("  skip:", message["Subject"])
            box.store(message_id, "+FLAGS", "\\Seen")
            continue

        name, address = email.utils.parseaddr(message["From"])
        day, words, where = for_today()

        letter = build_reply(user, address, message["Subject"], words)

        # These two headers put the answer inside the same conversation, so
        # the mail program shows question and answer together instead of apart.
        if message["Message-ID"]:
            letter["In-Reply-To"] = message["Message-ID"]
            letter["References"] = message["Message-ID"]

        with smtplib.SMTP_SSL(SMTP_HOST, 465) as server:
            server.login(user, password)
            server.send_message(letter)

        box.store(message_id, "+FLAGS", "\\Seen")
        sent = sent + 1
        print("  sent:", day, "horoscope to", address, "(" + where + ")")

    box.logout()
    return sent


def main():
    sent = one_pass(os.environ["MAIL_USER"], os.environ["MAIL_PASS"])
    print("horoscopes sent this pass:", sent)


if __name__ == "__main__":
    main()
LineWhat it does · ทำอะไร · 做什么
box.select("INBOX")Not readonly now. Anything handled gets marked read, so it is handled once.คราวนี้ไม่ใช่อ่านอย่างเดียว อะไรที่จัดการแล้วจะถูกทำเครื่องหมายว่าอ่านแล้ว จะได้จัดการครั้งเดียว这次不是只读。处理过的会被标成已读,所以只处理一次。
if ours_already(message):The one line that stands between you and a full inbox.บรรทัดเดียวที่กั้นระหว่างคุณกับกล่องจดหมายที่เต็ม挡在你和爆满收件箱之间的,就这一行。
letter["In-Reply-To"] = message["Message-ID"]Keeps question and answer together in the mail program.ทำให้คำถามกับคำตอบอยู่ด้วยกันในโปรแกรมอ่านเมล让问题和回答在邮件程序里待在一起。
box.store(message_id, "+FLAGS", "\\Seen")Mark it even when skipping, or every pass looks at it again.ทำเครื่องหมายแม้ตอนที่ข้าม ไม่อย่างนั้นทุกรอบจะกลับมาดูมันอีก就算跳过也要标记,不然每一轮都会再看它一遍。

🖥️ ONE PASS, FIVE LETTERS, ONE ANSWER

asking for a horoscope: 5
  skip: Re: horoscope please [horoscope-bot]
  skip: Re: Re: horoscope please [horoscope-bot]
  skip: Out of office: Re: horoscope please [horoscope-bot]
  skip: Fwd: Re: horoscope please [horoscope-bot]
  sent: 2026-09-23 horoscope to you@yahoo.com (from the file)
horoscopes sent this pass: 1
FIVE LETTERS, AND IT GOT ALL FIVE RIGHT Four of them quoted the tag back, in four different shapes. A reply. A reply to a reply. A forward. An out of office. All four skipped. The fifth was a person asking for the first time. It got a horoscope. A guard that blocks everything is easy. This one still answers. สี่ฉบับยกแท็กกลับมาด้วย ในสี่รูปแบบที่ต่างกัน การตอบกลับ การตอบกลับของการตอบกลับ การฟอร์เวิร์ด และข้อความไม่อยู่ออฟฟิศ ทั้งสี่ถูกข้ามหมด ส่วนฉบับที่ห้าคือคนที่เพิ่งถามเป็นครั้งแรก และได้ดวงไป ตัวกันที่บล็อกทุกอย่างนั้นทำง่าย แต่อันนี้ยังตอบอยู่ 其中四封把标签引了回来,四种不同的样子:回复、回复的回复、转发,还有外出自动回复。四封全部跳过。第五封是第一次来问的人,拿到了运势。一个什么都拦的守卫很好写,这一个还会回答。
YOUR TURN
  1. Email yourself asking for a horoscope. Run it once.
  2. Check: you get a reply, in the same conversation.
  3. Run it again straight away. It should skip its own letter.
  4. Reply to the horoscope. Run it again. It should skip that too.
  1. ส่งเมลหาตัวเองเพื่อขอดวง แล้วรันหนึ่งครั้ง
  2. ตรวจ: ต้องได้คำตอบกลับมา ในบทสนทนาเดียวกัน
  3. รันอีกครั้งทันที มันต้องข้ามจดหมายของตัวเอง
  4. ตอบกลับดวงนั้น แล้วรันอีกครั้ง มันต้องข้ามอันนั้นด้วย
  1. 给自己发一封要运势的邮件,跑一次。
  2. 检查:你收到回复,而且在同一个会话里。
  3. 马上再跑一次,它应该跳过自己那封。
  4. 回复那封运势邮件,再跑一次,它也该跳过。

5 Once an hour, for as long as you leave it on · ชั่วโมงละครั้ง ตราบเท่าที่คุณเปิดทิ้งไว้ · 每小时一次,只要你让它开着

🇬🇧 English

The loop is four lines. Look, report, sleep, repeat.

The try matters more than it looks. A bot meant to run for weeks will meet a dropped connection, and a bot that dies on one bad night is worse than one that shrugs and waits an hour.

🇹🇭 ไทย

ลูปนี้มีสี่บรรทัด ดู รายงาน หลับ แล้ววนใหม่

try สำคัญกว่าที่เห็น บอทที่ตั้งใจให้รันเป็นสัปดาห์ย่อมเจอเน็ตหลุด และบอทที่ตายเพราะคืนเดียวที่แย่ ก็แย่กว่าบอทที่ยักไหล่แล้วรอไปอีกชั่วโมง

🇨🇳 中文

循环只有四行:看、报告、睡、再来一遍。

那个 try 比看上去重要。一个打算跑几周的机器人一定会碰到断线,而因为一个糟糕的夜晚就死掉的机器人,比耸耸肩再等一小时的差得多。

📄 NEW FILE · bot3.py

"""bot3.py — the same pass, once an hour, for as long as you leave it on."""
import os
import sys
import time

from bot2 import one_pass

EVERY = 3600          # seconds between looks. 3600 is one hour.

sys.stdout.reconfigure(encoding="utf-8")


def main():
    """Look, answer, wait. Forever, or until you press Ctrl+C."""
    user = os.environ["MAIL_USER"]
    password = os.environ["MAIL_PASS"]

    while True:
        clock = time.strftime("%H:%M:%S")
        try:
            sent = one_pass(user, password)
            print(clock, "done.", sent, "sent. Sleeping", EVERY, "seconds.")
        except Exception as problem:
            # A bot that dies on one bad night is worse than a bot that
            # shrugs and tries again in an hour. Say what happened, carry on.
            print(clock, "that pass failed:", problem)
            print(clock, "trying again in", EVERY, "seconds.")

        time.sleep(EVERY)


if __name__ == "__main__":
    main()
LineWhat it does · ทำอะไร · 做什么
EVERY = 3600One hour, in seconds. Make it 60 while you are testing, then put it back.หนึ่งชั่วโมง คิดเป็นวินาที ตอนทดสอบตั้งเป็น 60 แล้วค่อยเปลี่ยนกลับ一小时,按秒算。测试时改成 60,之后再改回来。
except Exception as problem:Broad on purpose. Anything at all, and it still wakes up next hour.จงใจให้กว้าง เกิดอะไรขึ้นก็ตาม มันยังตื่นมาทำงานในชั่วโมงถัดไป故意写得很宽。不管出什么事,它下一小时还是会醒。
time.sleep(EVERY)The whole scheduler. No library, and nothing to install.ตัวจับเวลาทั้งหมดอยู่ตรงนี้ ไม่ต้องใช้ไลบรารี ไม่ต้องติดตั้งอะไร整个调度器就这个。不用库,什么都不用装。

🖥️ TWO PASSES, WITH THE TIMER TURNED DOWN TO 20 SECONDS

asking for a horoscope: 0
11:43:27 done. 0 sent. Sleeping 20 seconds.
asking for a horoscope: 0
11:43:51 done. 0 sent. Sleeping 20 seconds.
SLEEP IS NOT A SCHEDULER This only runs while the window is open. The machine must be awake too. Close the laptop and the bot stops. That is fine while you are building it. To run it whether you are there or not, hand it over. launchd on a Mac, cron on Linux. Let that start bot2.py once an hour. อันนี้ทำงานเฉพาะตอนที่หน้าต่างเปิดอยู่และเครื่องยังไม่หลับ ปิดฝาแล็ปท็อปเมื่อไหร่ บอทก็หยุด ตอนกำลังสร้างก็ถือว่าโอเค แต่ถ้าอยากให้มันทำงานไม่ว่าคุณจะอยู่หรือไม่ ให้ยกไปให้ launchd บน Mac หรือ cron บน Linux แล้วให้ตัวนั้นเรียก bot2.py ชั่วโมงละครั้งแทน 这个只在窗口开着、机器醒着的时候跑。合上笔记本,机器人就停了。做开发的时候这样没问题。要让它不管你在不在都跑,就交给 Mac 上的 launchd 或者 Linux 上的 cron,让它每小时启动一次 bot2.py。
YOUR TURN
  1. Set EVERY to 60 and leave it running while you make tea.
  2. Send yourself a request while it is asleep. Watch it get picked up.
  3. Unplug the network for a pass. Check: it complains and carries on.
  4. Put EVERY back to 3600 before you forget.
  1. ตั้ง EVERY เป็น 60 แล้วปล่อยรันไว้ระหว่างไปชงชา
  2. ส่งคำขอหาตัวเองตอนที่มันหลับอยู่ แล้วดูมันเก็บไปทำ
  3. ลองถอดเน็ตออกหนึ่งรอบ ตรวจ: มันต้องบ่นแล้วไปต่อ
  4. ตั้ง EVERY กลับเป็น 3600 ก่อนจะลืม
  1. 把 EVERY 设成 60,去泡茶,让它跑着。
  2. 趁它睡着的时候给自己发个请求,看它醒来处理掉。
  3. 拔掉网络跑一轮。检查:它会抱怨,然后继续。
  4. 趁还没忘,把 EVERY 改回 3600。

🤖 Tutorial 5 — Let the model read the subject · บทที่ 5 — ให้โมเดลอ่านหัวข้อ · 教程 5 —— 让模型读标题

🇬🇧 English

The word check has a hole in it. Somebody writes read my stars and the bot never sees them.

You already run a model for the horoscope. Ask it this question too.

🇹🇭 ไทย

การตรวจด้วยคำมีช่องโหว่ ถ้ามีคนเขียนว่า read my stars บอทจะไม่เห็นเขาเลย

คุณรันโมเดลไว้เขียนดวงอยู่แล้ว ก็ถามคำถามนี้กับมันด้วยเลย

🇨🇳 中文

用词去筛是有漏洞的。有人写 read my stars,机器人根本看不见他。

你本来就在跑一个模型写运势,顺便也问它这个问题。

6 Ask the model, not the word · ถามโมเดล ไม่ใช่ถามคำ · 问模型,别问词

🇬🇧 English

The question goes to the same Ollama that writes the horoscope. No new program, no new model.

One thing matters more than the model. Tell it the situation, not just the subject line.

Asked bare, it scored 74%. Given the situation first, 93%. Same model, same subjects, different wording.

🇹🇭 ไทย

คำถามส่งไปที่ Ollama ตัวเดียวกับที่เขียนดวง ไม่มีโปรแกรมใหม่ ไม่มีโมเดลใหม่

มีเรื่องหนึ่งสำคัญกว่าตัวโมเดล คือบอกสถานการณ์ให้มันรู้ ไม่ใช่ส่งแค่บรรทัดหัวข้อ

ถามลอย ๆ ได้ 74% บอกสถานการณ์ก่อนได้ 93% โมเดลเดิม หัวข้อเดิม ต่างกันที่คำถาม

🇨🇳 中文

问题发给写运势的那同一个 Ollama。不加程序,不加模型。

有件事比模型本身更重要:把情境告诉它,别只丢一行标题。

干问,74%。先讲清楚情境,93%。同一个模型,同一批标题,只是问法不同。

📄 NEW FILE · judge.py

"""judge.py — ask the model whether a subject line is asking for a horoscope."""
import json
import sys
import urllib.request

OLLAMA = "http://localhost:11434/api/chat"
MODEL = "llama3.2"

sys.stdout.reconfigure(encoding="utf-8")

# The model is told the situation, not just the subject. That one change took
# it from guessing to reliable, and step 6 shows the measurement.
ASK = """A robot answers emails for a daily horoscope service. It has received
an email with this subject line:
{subject}

Should the robot send this person a horoscope? Answer with JSON only:
{{"send": true or false, "confidence": a number from 0 to 1}}"""


def wants_horoscope(subject):
    """Is this person asking for a horoscope? Returns the answer and how sure."""
    payload = json.dumps({
        "model": MODEL,
        "stream": False,
        "format": "json",              # makes the reply parseable JSON
        "options": {"temperature": 0},  # same subject, same answer, every time
        "messages": [{"role": "user", "content": ASK.format(subject=subject)}],
    }).encode()
    request = urllib.request.Request(
        OLLAMA, data=payload, headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(request, timeout=300) as response:
        reply = json.load(response)["message"]["content"]

    answer = json.loads(reply)
    send = answer.get("send", False)
    if isinstance(send, str):
        # Some models write "true" as a word instead of a true value.
        send = send.strip().lower() in ("true", "yes")
    return bool(send), float(answer.get("confidence", 0))


def main():
    """Try it on subjects the word horoscope alone cannot sort out."""
    subjects = [
        "horoscope please",
        "read my stars",
        "what's in store for me today",
        "ดูดวงวันนี้หน่อย",
        "今天的运势如何?",
        "Unsubscribe me from the Daily Horoscope newsletter",
        "my horoscope was wrong yesterday",
        "Your package has shipped",
    ]
    for subject in subjects:
        send, confidence = wants_horoscope(subject)
        word = "horoscope" in subject.lower()
        print(f"{'reply ' if send else 'ignore'}  model {confidence:4.2f}  "
              f"word {'yes' if word else 'no ':3}  {subject}")


if __name__ == "__main__":
    main()
LineWhat it does · ทำอะไร · 做什么
ASK = """A robot answers emails for a daily horoscope service.The situation, in the question. This is the part that did the work.สถานการณ์อยู่ในคำถาม นี่คือส่วนที่ทำให้มันทำงานได้情境写在问题里。真正起作用的就是这部分。
"format": "json",Ollama returns parseable JSON. You still have to read it carefully.Ollama จะคืน JSON ที่แปลงค่าได้ แต่คุณยังต้องอ่านมันอย่างระวังOllama 会返回能解析的 JSON。你仍然要小心地读它。
"options": {"temperature": 0},Zero, so the same subject gets the same answer every time.ศูนย์ เพื่อให้หัวข้อเดิมได้คำตอบเดิมทุกครั้ง设成零,同一个标题每次都得到同一个答案。
if isinstance(send, str):Some models write the word true instead of a true value.บางโมเดลเขียนคำว่า true แทนที่จะให้ค่าจริง有些模型会写出 true 这个词,而不是真正的真值。

🖥️ WHAT IT PRINTED · eight subjects, and what the word check would say

reply   model 0.80  word yes  horoscope please
reply   model 0.80  word no   read my stars
reply   model 0.80  word no   what's in store for me today
reply   model 0.80  word no   ดูดวงวันนี้หน่อย
reply   model 0.80  word no   今天的运势如何?
ignore  model 0.20  word yes  Unsubscribe me from the Daily Horoscope newsletter
ignore  model 0.20  word yes  my horoscope was wrong yesterday
ignore  model 0.20  word no   Your package has shipped

🇬🇧 English

Read the word column. It is wrong five times out of eight, in both directions.

🇹🇭 ไทย

ดูคอลัมน์ word มันผิดห้าจากแปด และผิดทั้งสองทาง

🇨🇳 中文

看 word 那一列。八次错五次,两个方向都错。

Judgeตัวตัดสิน裁判Accuracyความแม่น准确率Speedความเร็ว速度Downloadต้องโหลด要下载
the word horoscopeคำว่า horoscopehoroscope 这个词26 of 57 rightถูก 26 จาก 5757 个对 26 个instantทันที瞬间nothing to installไม่ต้องติดตั้งอะไร什么都不用装
kev-0.6b, a decision modelkev-0.6b โมเดลตัดสินใจkev-0.6b,一个决策模型51 of 57 rightถูก 51 จาก 5757 个对 51 个234 ms each234 ms ต่อฉบับ每条 234 毫秒375 MB375 MB375 MB
llama3.2 through Ollamallama3.2 ผ่าน Ollama经由 Ollama 的 llama3.257 of 57 rightถูก 57 จาก 5757 个全对1.6 s each1.6 วินาทีต่อฉบับ每条 1.6 秒2 GB2 GB2 GB
THE CONFIDENCE NUMBER IS NOT A DIAL Look at the output again. Every yes says 0.80 and every no says 0.20. The model is not measuring anything. It learned that confident writing has numbers in it. Use the yes or no. Do not build a threshold on that number. There is nothing underneath it. ลองดูผลลัพธ์อีกครั้ง ทุก yes บอก 0.80 และทุก no บอก 0.20 โมเดลไม่ได้วัดอะไรเลย มันแค่เรียนรู้ว่าการเขียนอย่างมั่นใจมักมีตัวเลข ให้ใช้แค่ yes หรือ no อย่าไปตั้งเกณฑ์จากตัวเลขนั้น เพราะใต้มันไม่มีอะไรอยู่ 再看一次输出。每个 yes 都是 0.80,每个 no 都是 0.20。模型没有在测量任何东西,它只是学到自信的写法里会带数字。用 yes 或 no 就好。别拿那个数字去设阈值,因为它下面什么都没有。
YOUR TURN
  1. Run it. Check: the word column is wrong five times.
  2. Add a subject of your own to the list, and run it again.
  3. Change ASK to just the subject line. Watch it get worse.
  4. Put the situation back. That one paragraph is doing the work.
  1. รันดู ตรวจ: คอลัมน์ word ต้องผิดห้าครั้ง
  2. เพิ่มหัวข้อของคุณเองลงในลิสต์ แล้วรันใหม่
  3. เปลี่ยน ASK ให้เหลือแค่บรรทัดหัวข้อ แล้วดูมันแย่ลง
  4. ใส่สถานการณ์กลับเข้าไป ย่อหน้านั้นแหละที่ทำงานอยู่
  1. 运行它。检查:word 那列错五次。
  2. 往列表里加一个你自己的标题,再运行一次。
  3. 把 ASK 改成只有标题行,看它变差。
  4. 再把情境加回去。起作用的就是那一段。

7 The bot that asks before it answers · บอทที่ถามก่อนตอบ · 先问再答的机器人

🇬🇧 English

One thing has to change. The bot can no longer search for the word horoscope.

But it cannot read 2,251 unread emails either. So it filters by date instead: today only, which was four.

Cheap checks first. Our own tag costs nothing to look for. Only then does it spend a second asking the model.

🇹🇭 ไทย

มีสิ่งหนึ่งที่ต้องเปลี่ยน บอทจะค้นหาคำว่า horoscope ไม่ได้อีกแล้ว

แต่มันก็อ่านเมลที่ยังไม่ได้อ่าน 2,251 ฉบับไม่ไหวเหมือนกัน จึงเปลี่ยนไปกรองด้วยวันที่แทน เอาแค่วันนี้ ซึ่งมีอยู่สี่ฉบับ

ตรวจของถูกก่อน แท็กของเราเองหาได้ฟรี แล้วค่อยจ่ายหนึ่งวินาทีไปถามโมเดล

🇨🇳 中文

有一件事必须变:机器人不能再搜 horoscope 这个词了。

可它也读不完 2,251 封未读。于是改成按日期筛:只要今天的,那天是四封。

先做便宜的检查。找自己的标签不花钱。然后才花一秒钟去问模型。

📄 NEW FILE · bot4.py

"""bot4.py — one pass, but the model reads the subject instead of a word."""
import email
import email.policy
import email.utils
import imaplib
import os
import smtplib
import sys
import time

from horoscope import for_today
from judge import wants_horoscope
from mark import build_reply, ours_already

IMAP_HOST = "imap.gmail.com"      # yahoo: imap.mail.yahoo.com
SMTP_HOST = "smtp.gmail.com"      # yahoo: smtp.mail.yahoo.com

sys.stdout.reconfigure(encoding="utf-8")


def one_pass(user, password):
    """Look at today's unread mail, and answer the people asking."""
    box = imaplib.IMAP4_SSL(IMAP_HOST)
    box.login(user, password)
    box.select("INBOX")

    # Today only. The word horoscope can no longer do the filtering, because
    # the whole point is to catch the people who never say it. A date still
    # can: this mailbox holds 2,251 unread emails and 4 of them are today's.
    today = time.strftime("%d-%b-%Y")
    status, ids = box.search(None, "UNSEEN", "SINCE", today)
    waiting = ids[0].split()
    print("unread today:", len(waiting))

    sent = 0
    for message_id in waiting:
        status, raw = box.fetch(message_id, "(BODY.PEEK[HEADER])")
        message = email.message_from_bytes(raw[0][1], policy=email.policy.default)
        subject = message["Subject"] or ""

        # The free check first. Asking the model costs a second and a half,
        # and our own mail never needs asking about.
        if ours_already(message):
            print("  ours :", subject[:52])
            box.store(message_id, "+FLAGS", "\\Seen")
            continue

        answer, confidence = wants_horoscope(subject)
        if not answer:
            # Somebody else's mail. Leave it exactly as we found it, unread.
            print(f"  leave: {subject[:52]}")
            continue

        name, address = email.utils.parseaddr(message["From"])
        day, words, where = for_today()
        letter = build_reply(user, address, subject, words)
        if message["Message-ID"]:
            letter["In-Reply-To"] = message["Message-ID"]
            letter["References"] = message["Message-ID"]

        with smtplib.SMTP_SSL(SMTP_HOST, 465) as server:
            server.login(user, password)
            server.send_message(letter)

        box.store(message_id, "+FLAGS", "\\Seen")
        sent = sent + 1
        print(f"  reply: {subject[:52]}")

    box.logout()
    return sent


def main():
    sent = one_pass(os.environ["MAIL_USER"], os.environ["MAIL_PASS"])
    print("horoscopes sent this pass:", sent)


if __name__ == "__main__":
    main()
LineWhat it does · ทำอะไร · 做什么
status, ids = box.search(None, "UNSEEN", "SINCE", today)A date filters on the server too. It does not need the word.วันที่ก็กรองบนเซิร์ฟเวอร์ได้เหมือนกัน และไม่ต้องพึ่งคำนั้น日期同样能在服务器上筛,而且不依赖那个词。
if ours_already(message):The free check goes first. Our own mail never needs asking about.ตรวจที่ฟรีมาก่อน เมลของเราเองไม่ต้องถามใคร免费的检查放前面。我们自己的信不用问。
answer, confidence = wants_horoscope(subject)A second and a half. Only for mail that got this far.หนึ่งวินาทีครึ่ง และเฉพาะกับเมลที่ผ่านมาถึงตรงนี้一秒半,而且只花在走到这一步的信上。
print(f" leave: {subject[:52]}")It leaves and says nothing else. No flag, no reply, no trace.มันปล่อยไว้แล้วไม่ทำอะไรอีก ไม่มีการทำเครื่องหมาย ไม่ตอบ ไม่ทิ้งร่องรอย它放过它,别的什么都不做。不标记,不回复,不留痕迹。

🖥️ ONE PASS · two requests that never say the word

unread today: 3
  reply: read my stars
  reply: ดูดวงวันนี้หน่อย
  leave: Quick question about the API
horoscopes sent this pass: 2
NEVER MARK SOMEBODY ELSE'S MAIL AS READ Look at what happens after leave. Nothing. The earlier bot marked everything it touched. That was safe then. It only ever touched its own mail. This one reads your whole inbox. Marking a stranger's email as read would hide it from you forever. ดูว่าเกิดอะไรขึ้นหลังคำว่า leave ไม่มีอะไรเลย บอทตัวก่อนทำเครื่องหมายทุกฉบับที่มันแตะ ซึ่งปลอดภัยตอนที่มันแตะแต่เมลตัวเอง ตัวนี้อ่านกล่องจดหมายของคุณทั้งกล่อง การทำเครื่องหมายว่าอ่านแล้วให้เมลของคนอื่นจะซ่อนมันจากคุณไปตลอดกาล 看 leave 之后发生了什么:什么都没有。之前那个机器人会标记它碰过的每一封,那时它只碰自己的信,所以安全。这一个读的是你整个收件箱。把陌生人的信标成已读,就等于永远把它藏起来了。
YOUR TURN
  1. Email yourself read my stars. Run it. It should reply.
  2. Email yourself something ordinary. Check: still unread.
  3. Swap bot4 into bot3.py and leave it running.
  4. Try a request in your own language. See whether it holds up.
  1. ส่งเมลหาตัวเองว่า read my stars แล้วรัน มันควรตอบ
  2. ส่งเมลธรรมดาหาตัวเอง ตรวจ: ต้องยังไม่ถูกอ่าน
  3. สลับ bot4 เข้าไปใน bot3.py แล้วปล่อยให้รัน
  4. ลองส่งคำขอด้วยภาษาของคุณเอง ดูว่ามันยังไหวไหม
  1. 给自己发一封 read my stars,运行,它该回复。
  2. 给自己发一封普通的信。检查:它还是未读。
  3. 把 bot4 换进 bot3.py,让它一直跑。
  4. 用你自己的语言试一个请求,看它撑不撑得住。

🇬🇧 English

Seven files, and a bot that answers one kind of letter and ignores the rest.

What to take away is the shape of the decision. Reading mail is easy. Knowing when not to act is the whole job. The guard stayed one word. The question grew into a model.

The horoscope is the least important part. Swap it for anything: a price, a summary, a booking. The loop guard stays exactly the same.

🇹🇭 ไทย

เจ็ดไฟล์ และบอทหนึ่งตัวที่ตอบจดหมายแบบเดียว และปล่อยที่เหลือไว้

สิ่งที่ควรจำคือรูปร่างของการตัดสินใจ การอ่านเมลนั้นง่าย การรู้ว่าเมื่อไหร่ไม่ควรทำอะไรต่างหากคืองานทั้งหมด ตัวกันยังเป็นคำเดียว ส่วนคำถามโตขึ้นเป็นโมเดล

ดวงประจำวันเป็นส่วนที่สำคัญน้อยที่สุด เปลี่ยนเป็นอะไรก็ได้ ราคา สรุปข่าว การจอง ส่วนกันวงวนยังเหมือนเดิมทุกประการ

🇨🇳 中文

七个文件,一个只回一种信、其余一概不管的机器人。

该记住的是那个判断的形状。读邮件很简单,知道什么时候不要动手才是全部的工作。防循环的那道关还是一个词,问题本身长成了一个模型。

运势是最不重要的部分。换成别的都行:价格、摘要、预约。防循环的那部分一个字都不用改。

You wantคุณอยากได้你想要Look upไปหาคำว่า去查
It to run without youให้มันทำงานโดยไม่ต้องมีคุณ不用你在也能跑launchd / cron
A different job per subjectงานคนละแบบตามหัวข้อ不同标题做不同的事more branches in ours_alreadyเพิ่มเงื่อนไขใน ours_already在 ours_already 里多加分支
Draw the whole thing insteadวาดทั้งหมดแทนการเขียน改成画出来n8n.htmln8n.htmln8n.html
The email code explainedคำอธิบายโค้ดอีเมล邮件代码的讲解send_email.htmlsend_email.htmlsend_email.html

Fifteen lines, your own address, and an app password. That is the whole trick. · สิบห้าบรรทัด อีเมลของคุณเอง และ app password หนึ่งอัน เคล็ดลับมีแค่นี้ · 十五行代码、你自己的邮箱地址,加一个应用专用密码。诀窍就这些。

All seven files on this page were run on 23 September 2026 and every output shown is copied from those runs. The page teaches the Gmail servers, the same as the send email page. The mailbox actually used was a Yahoo one holding 2,249 unread emails, so each file was run with its host lines swapped and nothing else changed. In the pass quoted in step 4 the bot was handed five letters at once - its own answer, a reply to that answer, a forward of it, a stranger’s out-of-office, and one person asking for the first time - and it skipped four and answered one. Two earlier designs were thrown away on the way: one kept a file of every Message-ID it had sent, and one hid the mark in white text on a white background. The file went when the Message-ID was given a fixed token; the hidden text went when SpamAssassin turned out to score it. The HEADER search failure in step 1 is a real error from that Yahoo mailbox. The accuracy figures in step 6 come from 57 subject lines in English, Thai and Chinese, scored against three judges. Twenty-seven of them were written first, and were used to choose the wording of the question; the other thirty were written afterwards and used to tune nothing. The kev-0.6b row is a real measurement of a purpose-built decision model, run on this laptop’s CPU. It is not what the bot uses, because on the thirty held-out subjects it made two confident mistakes - it was 93% sure that a Thai email asking what time tomorrow’s meeting starts wanted a horoscope.