So thanks to having a look at seance's design and having done some AI-assisted coding, I decided I would give it a whirl to have a local email handler last night (way too late).
I host my own mailserver anyway, so all my email is in ~/Maildir on my server. I had already installed ollama for an unrelated service, and had just heard of mu, a maildir tool.
So on a hunch I hacked to together this script.
import std/[os, osproc, httpclient, json, times, strutils]
# put together a prompt. this is very very very much experimental but it works pretty well already.
let prompt = """
Please decide what to do about this email, and place one of these tags in your reply. Do not otherwise mention tags as an automated system will only check for presence and then do what the tag says. Do not discuss anything, just output the tag and the described additional text.
ARCHIVE nothing needs to be done with this mail, just archive it. This is for TOC changes, miscellaneous transactional emails.
SUMMARY put a 10-20 word summary after the tag to log it for the user to glance over. This is the default. If there is an unsubscribe link, include it.
ACTION This is like summary but write the action you believe the email implies. This is for personal emails, invoices, ggovernment email, or any kind of deadline or request. If this looks like an invoice, include IBAN, recipient-name and amount.
EMERGENCY Emergency use only- will alert user immediately, and he very much doesn't like that. Only use if it is overwhelingly important to respond immediately to avoid major life disruption, e.g. immenent personal compulsary debt collection, important contract will get cancelled immediately.
Email text:
"""
while true:
# find unread mail and spit out date and filename
let(output1, status1) = execCmdEx """mu find flag:unread -s date -z -n 1 -f d,l"""
assert status1 == 0
let columns = output1.strip.split(',', maxsplit=1)
assert columns.len == 2
let
mailDate = columns[0]
filename = columns[1]
# use mu to decode it
# calling other executables from fast Nim code takes some getting used to!!!
let (output2, status2) = execCmdEx quoteShellCommand(@["mu","view", filename])
assert status2 == 0
let content = output2
let client = newHttpClient()
client.headers = newHttpHeaders({"Content-Type": "application/json"})
let body = %*{
#"model":"llama3:8b",
"model":"mistral",
"prompt": prompt & content,
"stream":false
}
# have the AI ponder the mail. Mistral is excellent.
let response = client.request("http://localhost:11434/api/generate", httpMethod = HttpPost, body = $body)
assert response.status == "200 OK"
let jb = parseJson(response.body)
let log = open("handlemail.log", fmAppend)
let moveTo = filename.replace("/Maildir/cur", "/Maildir/.Archive/cur") & "S"
log.writeLine(getStr(jb["created_at"]) & "\t" & mailDate & "\t" & moveTo & "\t" & getStr(jb["response"]).replace('\n', ' '))
log.close
# don't respond to the AI just yet, just archive it and mark it unread, maildir style
discard execCmd quoteShellCommand(@["mu","remove",filename])
moveFile(filename, moveTo)
discard execCmd quoteShellCommand(@["mu","add",moveTo])
# now I will read handlemail.log instead of checking my email for a while and see if anything bad happens
So I can certify that Nim is not only for lofty clean code but also excels at extremely dirty hacks!
So far I am really really happy with it. Just getting the summaries has made it much easier to see if I need to go dig into the mail client for the original. I don't think there's any obvious things the summary wouldn't catch that I need to act on. This only works with mistral 7b, llama3 results are pretty bad. Missing is all the basics, even handling the case where all mail is archived. I wouldn't make any action decisions based on output yet, but with more experience- sure. Much improvement possible in the workflow. But woah- look ma, self hosted AI!!!