Plugin Examples
This page contains complete, working plugin examples that you can use as starting points.
Links Plugin
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:
| Plugin | Filter Criteria | Use Case |
|---|---|---|
| Travel | Airlines, hotels, booking sites | Trip itineraries |
| Social | Twitter, LinkedIn, Facebook | Social notifications |
| Shipping | UPS, FedEx, USPS, DHL | Package tracking |
| Banking | Bank domains, "statement" | Financial alerts |
| Calendar | .ics attachments, "invitation" | Event invites |
| Support | "ticket", "case #", support domains | Support tickets |
| Jobs | LinkedIn, Indeed, job boards | Job 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.