mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
163 lines
7.8 KiB
Python
Executable File
163 lines
7.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Write the deterministic Maildir used for docs/performance.md.
|
|
|
|
The mailbox mixes HTML newsletters, short HTML notifications and plain-text
|
|
conversations, some with PDF attachments, dated over the last 60 days. Serve
|
|
it with any IMAP server that reads Maildir (the measurements used Dovecot).
|
|
|
|
scripts/generate-benchmark-mailbox.py ~/bench/Maildir
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import email.utils
|
|
import os
|
|
import random
|
|
import time
|
|
from email.message import EmailMessage
|
|
from pathlib import Path
|
|
|
|
SENDERS = [
|
|
("Ana Ruiz", "ana@northwind.example"),
|
|
("Ben Ortiz", "ben@contoso.example"),
|
|
("Chloe Martin", "chloe@fabrikam.example"),
|
|
("Dmitri Ivanov", "dmitri@adatum.example"),
|
|
("Elena Rossi", "elena@litware.example"),
|
|
("Farah Khan", "farah@proseware.example"),
|
|
("Gwen Lee", "gwen@tailspin.example"),
|
|
("Hugo Silva", "hugo@wingtip.example"),
|
|
]
|
|
LISTS = [
|
|
("Rust Weekly", "news@rustweekly.example"),
|
|
("Design Digest", "hello@designdigest.example"),
|
|
("Cloud Status", "status@cloudhost.example"),
|
|
("Shop Deals", "deals@shop.example"),
|
|
("Dev Events", "events@devconf.example"),
|
|
("Bank Notices", "notice@bank.example"),
|
|
]
|
|
WORDS = (
|
|
"release roadmap invoice meeting sprint review design budget travel report "
|
|
"draft summary feedback launch update contract quarterly agenda notes metrics "
|
|
"deploy migration customer support partner workshop hiring planning"
|
|
).split()
|
|
|
|
|
|
class Generator:
|
|
def __init__(self, seed: int, now: float) -> None:
|
|
self.rng = random.Random(seed)
|
|
self.now = now
|
|
|
|
def sentence(self, words: int = 12) -> str:
|
|
return " ".join(self.rng.choice(WORDS) for _ in range(words)).capitalize() + "."
|
|
|
|
def paragraph(self, sentences: int = 4) -> str:
|
|
return " ".join(self.sentence(self.rng.randint(8, 18)) for _ in range(sentences))
|
|
|
|
def newsletter(self, title: str) -> str:
|
|
stories = []
|
|
for index in range(self.rng.randint(4, 9)):
|
|
stories.append(f"""
|
|
<tr><td style="padding:16px 24px;border-bottom:1px solid #e5e7eb">
|
|
<h2 style="font:600 18px/1.3 Helvetica,Arial,sans-serif;color:#111827;margin:0 0 8px">{self.sentence(6)}</h2>
|
|
<p style="font:15px/1.6 Helvetica,Arial,sans-serif;color:#374151;margin:0 0 12px">{self.paragraph(3)}</p>
|
|
<a href="https://example.com/story/{index}" style="display:inline-block;background:#2563eb;color:#fff;
|
|
padding:8px 14px;border-radius:6px;text-decoration:none;font:600 14px Helvetica,Arial">Read more</a>
|
|
</td></tr>""")
|
|
return f"""<!doctype html><html><head><meta charset="utf-8">
|
|
<style>@media (max-width:600px){{.wrap{{width:100%!important}}}} a{{color:#2563eb}}</style></head>
|
|
<body style="margin:0;background:#f3f4f6">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" style="background:#f3f4f6"><tr><td align="center">
|
|
<table class="wrap" role="presentation" width="600" cellpadding="0" cellspacing="0" style="background:#ffffff;margin:24px 0">
|
|
<tr><td style="padding:24px;background:#111827;color:#fff;font:700 22px Helvetica,Arial">{title}</td></tr>
|
|
{''.join(stories)}
|
|
<tr><td style="padding:16px 24px;font:12px/1.5 Helvetica,Arial;color:#6b7280">
|
|
You are receiving this because you subscribed. <a href="https://example.com/unsubscribe">Unsubscribe</a>.
|
|
<img src="https://tracker.example.com/open.gif" width="1" height="1" alt=""></td></tr>
|
|
</table></td></tr></table></body></html>"""
|
|
|
|
def notification(self) -> str:
|
|
return f"""<html><body style="font-family:Arial,sans-serif">
|
|
<div style="max-width:520px;margin:0 auto;padding:20px;border:1px solid #ddd;border-radius:8px">
|
|
<p style="font-size:16px"><b>{self.sentence(5)}</b></p><p>{self.paragraph(2)}</p>
|
|
<p><a href="https://example.com/view">View details</a></p></div></body></html>"""
|
|
|
|
def message(self, index: int) -> tuple[float, bytes, str]:
|
|
kind = self.rng.random()
|
|
sent = self.now - self.rng.uniform(0, 60 * 86400)
|
|
message = EmailMessage()
|
|
message["Date"] = email.utils.formatdate(sent)
|
|
message["To"] = "Test User <testuser@localhost>"
|
|
message["Message-ID"] = f"<ram-{index}@flectar.test>"
|
|
if kind < 0.45:
|
|
name, address = self.rng.choice(LISTS)
|
|
message["From"] = f"{name} <{address}>"
|
|
message["Subject"] = f"{name}: {self.sentence(5)[:-1]}"
|
|
message["List-Unsubscribe"] = "<https://example.com/unsubscribe>"
|
|
message.set_content(self.paragraph(6))
|
|
message.add_alternative(self.newsletter(name), subtype="html")
|
|
elif kind < 0.65:
|
|
name, address = self.rng.choice(LISTS)
|
|
message["From"] = f"{name} <{address}>"
|
|
message["Subject"] = self.sentence(6)[:-1]
|
|
message.set_content(self.paragraph(2))
|
|
message.add_alternative(self.notification(), subtype="html")
|
|
else:
|
|
name, address = self.rng.choice(SENDERS)
|
|
message["From"] = f"{name} <{address}>"
|
|
subject = self.sentence(5)[:-1]
|
|
if self.rng.random() < 0.35 and index > 10:
|
|
parent = self.rng.randint(0, index - 1)
|
|
message["In-Reply-To"] = f"<ram-{parent}@flectar.test>"
|
|
message["References"] = f"<ram-{parent}@flectar.test>"
|
|
subject = "Re: " + subject
|
|
message["Subject"] = subject
|
|
first = name.split()[0]
|
|
message.set_content(f"Hi,\n\n{self.paragraph(3)}\n\n{self.paragraph(2)}\n\nThanks,\n{first}\n")
|
|
if self.rng.random() < 0.12:
|
|
size = self.rng.randint(20_000, 120_000)
|
|
data = bytes(self.rng.getrandbits(8) for _ in range(size))
|
|
message.add_attachment(
|
|
data, maintype="application", subtype="pdf", filename=f"{self.rng.choice(WORDS)}-{index}.pdf"
|
|
)
|
|
flags = "S" if self.rng.random() < 0.8 else ""
|
|
if self.rng.random() < 0.05:
|
|
flags = "F" + flags
|
|
return sent, message.as_bytes(), flags
|
|
|
|
def write_folder(self, folder: Path, first: int, count: int) -> None:
|
|
for name in ("cur", "new", "tmp"):
|
|
(folder / name).mkdir(parents=True, exist_ok=True)
|
|
for index in range(first, first + count):
|
|
sent, raw, flags = self.message(index)
|
|
path = folder / "cur" / f"{int(sent)}.M{index}P1.flectar-bench,S={len(raw)}:2,{flags}"
|
|
path.write_bytes(raw)
|
|
# Maildir servers report the file time as the received date.
|
|
os.utime(path, (sent, sent))
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("maildir", type=Path, help="Maildir to create; must not contain mail yet")
|
|
parser.add_argument("--inbox", type=int, default=600, help="Inbox messages (default 600)")
|
|
parser.add_argument("--seed", type=int, default=20261103)
|
|
args = parser.parse_args()
|
|
if args.inbox < 1 or any((args.maildir / "cur").glob("*")):
|
|
parser.error("use an empty Maildir and at least one Inbox message")
|
|
# The email package draws MIME boundaries from the global generator.
|
|
random.seed(args.seed)
|
|
generator = Generator(args.seed, time.time())
|
|
archive, sent = args.inbox // 3, args.inbox // 10
|
|
generator.write_folder(args.maildir, 0, args.inbox)
|
|
generator.write_folder(args.maildir / ".Archive", args.inbox, archive)
|
|
generator.write_folder(args.maildir / ".Sent", args.inbox + archive, sent)
|
|
for folder in (".Drafts", ".Trash", ".Junk"):
|
|
for name in ("cur", "new", "tmp"):
|
|
(args.maildir / folder / name).mkdir(parents=True, exist_ok=True)
|
|
print(f"Wrote {args.inbox + archive + sent} messages to {args.maildir}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|