Skip to main content

Plugin Examples

This page contains complete, working plugin examples that you can use as starting points.

The Links plugin creates a virtual folder for emails you sent to yourself containing URLs - perfect for saving articles to read later.

~/.gaivota/plugins/links.lua
metadata = {
name = "Links",
description = "Shows self-sent emails with links as clickable cards",
version = "1.0",
author = "Gaivota",
creates = {
virtual_folder = {
name = "Links",
description = "Emails you sent to yourself containing URLs",
icon = "link"
}
}
}

function filter(email, user_email)
-- Only include emails that are:
-- 1. From yourself
-- 2. To yourself
-- 3. Contain at least one URL
return email:from_self(user_email)
and email:to_self(user_email)
and email:has_links()
end

function render(email, metadata_list)
-- Use first link's metadata for list display
local first_meta = metadata_list[1]
local subject = email:subject() or "Link"
local sender = "Links"
local preview = ""

if first_meta and first_meta:has_metadata() then
if first_meta:title() then subject = first_meta:title() end
if first_meta:site_name() then sender = first_meta:site_name() end
if first_meta:description() then preview = first_meta:description() end
end

-- Build interactive HTML with link cards
local html = [[<div class="links-container" style="display: flex; flex-direction: column; gap: 16px; padding: 16px;">]]

for _, meta in ipairs(metadata_list) do
html = html .. [[
<a href="#"
data-action="mark-read-open-url"
data-url="]] .. meta:url() .. [["
class="link-card"
style="display: flex; gap: 12px; padding: 12px; border: 1px solid #e5e7eb; border-radius: 8px; text-decoration: none; color: inherit; transition: background-color 0.2s;">
]]

-- Add image if available
if meta:image() then
html = html .. [[<img src="]] .. meta:image() .. [["
style="width: 120px; height: 80px; object-fit: cover; border-radius: 4px; flex-shrink: 0;" />]]
end

html = html .. [[<div style="flex: 1; min-width: 0;">]]
html = html .. [[<div style="font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">]]
.. (meta:title() or meta:url()) .. [[</div>]]

if meta:description() then
html = html .. [[<div style="font-size: 14px; color: #6b7280; margin-top: 4px;
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;">]]
.. meta:description() .. [[</div>]]
end

html = html .. [[<div style="font-size: 12px; color: #9ca3af; margin-top: 4px;">]]
.. (meta:site_name() or "") .. [[</div>]]
html = html .. [[</div></a>]]
end

html = html .. [[</div>]]

return {
subject = subject,
sender = sender,
preview = preview,
body_html = html
}
end

GitHub Notifications

Filter and display GitHub notification emails with a clean interface.

~/.gaivota/plugins/github.lua
metadata = {
name = "GitHub",
description = "GitHub notifications and activity",
version = "1.0",
creates = {
virtual_folder = {
name = "GitHub",
description = "Notifications from GitHub",
icon = "github" -- or use "git-branch"
}
}
}

function filter(email, user_email)
local sender = email:sender():lower()
return sender:match("@github%.com$") ~= nil
or sender:match("noreply@github%.com") ~= nil
end

function render(email, metadata_list)
local subject = email:subject() or "GitHub Notification"
local preview = email:preview() or ""

-- Extract repo name from subject if possible
-- GitHub subjects look like: "[owner/repo] Title here"
local repo = subject:match("%[([^%]]+)%]")
local title = subject:gsub("^%[[^%]]+%]%s*", "")

local html = [[
<div style="padding: 16px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 12px;">
<svg width="20" height="20" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
</svg>
]]

if repo then
html = html .. [[<span style="font-weight: 500; color: #6b7280;">]] .. repo .. [[</span>]]
end

html = html .. [[
</div>
<h2 style="margin: 0 0 12px 0; font-size: 18px; font-weight: 600;">]] .. title .. [[</h2>
<div style="color: #6b7280; line-height: 1.5;">]] .. preview .. [[</div>
</div>
]]

return {
subject = title,
sender = repo or "GitHub",
preview = preview,
body_html = html
}
end

Receipts & Orders

Collect all purchase receipts and order confirmations in one place.

~/.gaivota/plugins/receipts.lua
metadata = {
name = "Receipts",
description = "Purchase receipts and order confirmations",
version = "1.0",
creates = {
virtual_folder = {
name = "Receipts",
description = "Order confirmations and receipts",
icon = "receipt" -- or "credit-card"
}
}
}

-- Common receipt-related keywords
local keywords = {
"receipt", "order confirmation", "order confirmed",
"your order", "purchase", "invoice", "payment received",
"thank you for your order", "shipping confirmation"
}

function filter(email, user_email)
local subject = (email:subject() or ""):lower()
local preview = (email:preview() or ""):lower()
local text = subject .. " " .. preview

for _, keyword in ipairs(keywords) do
if text:match(keyword) then
return true
end
end

return false
end

function render(email, metadata_list)
local subject = email:subject() or "Receipt"
local sender_name = email:sender_name() or email:sender()
local preview = email:preview() or ""

-- Try to extract order number
local order_num = subject:match("#(%d+)")
or subject:match("Order%s+(%d+)")
or preview:match("#(%d+)")

local html = [[
<div style="padding: 16px;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 16px;">
<div style="width: 48px; height: 48px; background: #f3f4f6; border-radius: 8px;
display: flex; align-items: center; justify-content: center;">
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="4" width="18" height="18" rx="2"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</div>
<div>
<div style="font-weight: 600;">]] .. sender_name .. [[</div>
]]

if order_num then
html = html .. [[<div style="font-size: 14px; color: #6b7280;">Order #]] .. order_num .. [[</div>]]
end

html = html .. [[
</div>
</div>
<h2 style="margin: 0 0 12px 0; font-size: 16px;">]] .. subject .. [[</h2>
<div style="color: #6b7280; font-size: 14px; line-height: 1.5;">]] .. preview .. [[</div>
</div>
]]

return {
subject = subject,
sender = sender_name,
preview = preview,
body_html = html
}
end

Newsletter Digest

Collect newsletters in a clean reading format.

~/.gaivota/plugins/newsletters.lua
metadata = {
name = "Newsletters",
description = "Newsletter subscriptions",
version = "1.0",
creates = {
virtual_folder = {
name = "Newsletters",
description = "Your newsletter subscriptions",
icon = "newspaper"
}
}
}

-- Common newsletter senders
local newsletter_domains = {
"substack.com",
"mailchimp.com",
"convertkit.com",
"buttondown.email",
"revue.email",
"beehiiv.com"
}

function filter(email, user_email)
local sender = email:sender():lower()

-- Check if from known newsletter platform
for _, domain in ipairs(newsletter_domains) do
if sender:match(domain) then
return true
end
end

-- Check for unsubscribe link (common newsletter indicator)
local preview = (email:preview() or ""):lower()
if preview:match("unsubscribe") then
return true
end

return false
end

function render(email, metadata_list)
local subject = email:subject() or "Newsletter"
local sender_name = email:sender_name() or email:sender()
local preview = email:preview() or ""

local html = [[
<div style="max-width: 600px; margin: 0 auto; padding: 24px;">
<div style="border-bottom: 1px solid #e5e7eb; padding-bottom: 16px; margin-bottom: 16px;">
<div style="font-size: 14px; color: #6b7280; margin-bottom: 4px;">]] .. sender_name .. [[</div>
<h1 style="margin: 0; font-size: 24px; font-weight: 700; line-height: 1.3;">]] .. subject .. [[</h1>
</div>
<div style="font-size: 16px; line-height: 1.6; color: #374151;">
]] .. preview .. [[
</div>
</div>
]]

return {
subject = subject,
sender = sender_name,
preview = preview,
body_html = html
}
end

Plugin Ideas

Here are more plugin ideas you can implement:

PluginFilter CriteriaUse Case
TravelAirlines, hotels, booking sitesTrip itineraries
SocialTwitter, LinkedIn, FacebookSocial notifications
ShippingUPS, FedEx, USPS, DHLPackage tracking
BankingBank domains, "statement"Financial alerts
Calendar.ics attachments, "invitation"Event invites
Support"ticket", "case #", support domainsSupport tickets
JobsLinkedIn, Indeed, job boardsJob applications

Tips for Writing Plugins

1. Be Specific with Filters

Narrow filters work better than broad ones:

-- Good: Specific domain check
return email:sender():match("@company%.com$")

-- Less good: Might catch unrelated emails
return email:subject():match("update")

2. Handle Missing Data

Always check for nil values:

local subject = email:subject() or "No Subject"
local preview = email:preview() or ""

3. Use CSS for Styling

Inline styles work best for email-like rendering:

local html = [[<div style="padding: 16px; font-family: system-ui;">]]

4. Test with print()

Debug your filters with print statements:

function filter(email, user_email)
print("Checking: " .. (email:subject() or "no subject"))
print("From: " .. email:sender())
-- ... rest of filter
end

Check the app logs to see the output.