Using Ruby and JMAP to do some basic email archiving
Sometimes when we’re monitoring a system, a quick and dirty way to keep an eye on things is an automated periodic email with some kind of status summary. These kind of monitors are super-easy to set up with a cron job on a server and less effort than implementing some kind of dashboard or integration with an API.
Perfect for a quick and dirty monitor to run temporarily for a few days or weeks while we’re keeping an eye on something.
But an hourly email creates a lot of noise in my inbox, and if I’m away for a while, I’m typically only interested in glancing at the most recent email. So I wanted a quick and easy way to archive all but the most recent email with a particular subject line.
If there are 10 emails in my inbox with Subject line “System status report from ACME service”, I’m happy for the 9 older ones to be archived without me even seeing them. But I’d quite like the latest one to stay in my inbox until I’ve reviewed it.
I initially looked at whether it was possible to achieve this with Fastmail’s built in filtering system, but like many email systems, that’s based on processing an email as it comes in, without the context of all the other emails in the folder.
Similarly, Thunderbird doesn’t have any built-in functionality that can do this.
So just for fun I thought I’d tried having a go at implementing a little script to do this. I assumed Fastmail would have some kind of API that I could connect with using Ruby, but I must confess I’d never heard of JMAP.
JMAP is a recently developed IETF standard that appears to have been driven by Fastmail, so it’s no surprise that JMAP seems to be the way to talk to the Fastmail API.
With the help of a few Python examples I found online, and the Ruby rest-client gem, I managed to whip up a little Ruby script to connect to Fastmail, find the most recent 10 emails with subject line X, and archive all but the latest one.
Here’s what I ended up with:
require 'rest-client'
require 'json'
require 'time'
JMAP_URL = 'https://api.fastmail.com/jmap/api/'
BEARER_TOKEN = 'fmu1-00000000-...'
ACCOUNT_ID = 'u12345678'
INBOX_ID = '...'
ARCHIVE_MAILBOX_ID = '...'
HEADERS = {
'Authorization' => "Bearer #{BEARER_TOKEN}",
'Content-Type' => 'application/json; charset=utf-8'
}
# Fetches the last N emails matching a subject from the inbox
def get_matching_emails(account_id, inbox_id, subject, limit = 50)
query_body = {
using: ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
methodCalls: [
[
"Email/query",
{
accountId: account_id,
filter: {
inMailbox: inbox_id,
subject: subject
},
sort: [{ property: "receivedAt", isAscending: false }],
limit: limit
},
"a"
]
]
}
response = RestClient.post(JMAP_URL, query_body.to_json, HEADERS)
result = JSON.parse(response.body)
# Extract the list of email IDs from the query result
result.dig('methodResponses', 0, 1, 'ids') || []
end
# Moves an email to the Archive mailbox
def move_email_to_archive(email_id, account_id, archive_mailbox_id)
move_query = {
using: ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
methodCalls: [
[
"Email/set",
{
accountId: account_id,
"update": {
email_id => {
"mailboxIds/#{ARCHIVE_MAILBOX_ID}" => true, # Adding email to Archive
"mailboxIds/#{INBOX_ID}" => nil # Removing email from Inbox
}
}
},
"a"
]
]
}
RestClient.post(JMAP_URL, move_query.to_json, HEADERS)
end
# Fetch the details of an email
def get_email_details(email_id, account_id)
email_query = {
using: ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
methodCalls: [
[
"Email/get",
{
accountId: account_id,
ids: [email_id],
properties: ["receivedAt"]
},
"a"
]
]
}
response = RestClient.post(JMAP_URL, email_query.to_json, HEADERS)
JSON.parse(response.body)
end
# Archives all matching emails except the most recent one
def archive_emails_except_most_recent(account_id, inbox_id, subject, archive_mailbox_id, limit = 50)
# Fetch the last N emails that match the subject
email_ids = get_matching_emails(account_id, inbox_id, subject, limit)
if email_ids.empty?
puts "No emails found matching the subject."
return
end
# The most recent email is the first one in the list
most_recent_email_id = email_ids.first
puts "Most recent email ID: #{most_recent_email_id}"
# Archive all emails except the most recent
email_ids[1..-1].each do |email_id|
email_details = get_email_details(email_id, account_id)
# Extract the receivedAt timestamp for the email
received_at = email_details.dig('methodResponses', 0, 1, 'list', 0, 'receivedAt')
next if received_at.nil?
puts "Archiving email ID #{email_id} (received at #{received_at})"
move_email_to_archive(email_id, account_id, archive_mailbox_id)
puts "Email ID #{email_id} moved to Archive."
end
end
archive_emails_except_most_recent(ACCOUNT_ID, INBOX_ID, "ACME server - status report", ARCHIVE_MAILBOX_ID)
This can be run as a scheduled task on a desktop computer, or (with appropriate consideration of security issues), run from a server in the cloud, or from a serverless platform like AWS Lambda.