1-- ai_rewrite.lua
2-- Rewrites the email body using an AI model.
3-- Press ctrl+r in the composer to open the prompt overlay.
4--
5-- Configuration: Set the API_URL, API_KEY, and MODEL variables below.
6-- Works with any OpenAI-compatible API (OpenAI, Ollama, llama.cpp, etc).
7
8local matcha = require("matcha")
9
10-- Configuration
11local API_URL = "http://localhost:11434/v1/chat/completions" -- Ollama default
12local API_KEY = "" -- not needed for Ollama
13local MODEL = "llama3"
14
15local SYSTEM_PROMPT = [[You are an email rewriting assistant inside an email client.
16
17You will receive an email body that the user has already written, along with an instruction on how to rewrite it. Your job is to rephrase the existing text according to the instruction.
18
19Critical rules:
20- Output ONLY the rewritten email body. Nothing else.
21- Do NOT include a subject line.
22- Do NOT include a signature, sign-off, closing, or sender name (e.g. "Best regards, X", "Sincerely, X", "Thanks, X"). The email client appends the signature automatically.
23- Do NOT include the sender's name or email address anywhere in the output.
24- Do NOT wrap the output in quotes, markdown code blocks, or any formatting markers.
25- Do NOT add any preamble like "Here is the rewritten email:" or "Your email:".
26- Do NOT invent, assume, or add any facts, details, or information not present in the original email body. Only rephrase what is already there.
27- If the recipient's name is needed for a greeting, extract it from the To address. Use only the first name. If no display name is available, omit the name from the greeting entirely (just use "Hi," or "Hello,").
28- Preserve the original intent, meaning, and all key information.
29- Match the tone and style requested by the user.
30- Keep similar length unless the user asks to shorten or expand.]]
31
32matcha.bind_key("ctrl+r", "composer", "ai rewrite", function(state)
33 matcha.prompt("Rewrite instruction (e.g. 'make it more formal'):", function(instruction)
34 local body = state.body
35 if body == "" then
36 matcha.notify("Nothing to rewrite", 2)
37 return
38 end
39
40 local user_msg = string.format(
41 "To: %s\nSubject: %s\nInstruction: %s\n\nEmail body:\n%s",
42 state.to, state.subject, instruction, body
43 )
44
45 local payload = string.format(
46 '{"model":"%s","messages":[{"role":"system","content":"%s"},{"role":"user","content":"%s"}]}',
47 MODEL,
48 SYSTEM_PROMPT:gsub('"', '\\"'):gsub('\n', '\\n'),
49 user_msg:gsub('"', '\\"'):gsub('\n', '\\n')
50 )
51
52 local headers = { ["Content-Type"] = "application/json" }
53 if API_KEY ~= "" then
54 headers["Authorization"] = "Bearer " .. API_KEY
55 end
56
57 matcha.notify("Rewriting...", 10)
58
59 local res, err = matcha.http({
60 url = API_URL,
61 method = "POST",
62 headers = headers,
63 body = payload,
64 })
65
66 if err then
67 matcha.notify("AI error: " .. err, 3)
68 return
69 end
70
71 if res.status ~= 200 then
72 matcha.notify("AI returned status " .. res.status, 3)
73 return
74 end
75
76 -- Extract content from OpenAI-compatible response.
77 -- Response format: {"choices":[{"message":{"content":"..."}}]}
78 local content = res.body:match('"content"%s*:%s*"(.-)"')
79 if not content then
80 matcha.notify("Could not parse AI response", 3)
81 return
82 end
83
84 -- Unescape JSON string
85 content = content:gsub('\\n', '\n')
86 content = content:gsub('\\"', '"')
87 content = content:gsub('\\\\', '\\')
88
89 matcha.set_compose_field("body", content)
90 matcha.notify("Email rewritten", 2)
91 end)
92end)