Email/SMTP/IMAP

work-in-progress

Rye has builtins for sending email via SMTP, reading email via IMAP, and parsing email files. This page covers the main operations.

Sending email (SMTP)

Use the gomail-based builtins to compose and send emails via SMTP.

Creating and configuring a message

; Create a new email message
email-message

; Set headers
|Set-header "Subject" "Hello from Clojure"
|Set-header 'To "recipient@example.com"
|Set-header 'From "sender@example.com"

; Or use address header with display name
|Set-address-header "From" "sender@example.com" "John Doe"
|Set-address-header "To" "recipient@example.com" "Jane Smith"

; Set the body - plain text
|Set-body "text/plain" "Hello, this is the email body."

; Or HTML body
|Set-body "text/html" "<h1>Hello</h1><p>This is HTML content.</p>"

Multipart emails (text + HTML)

email-message
|Set-header "Subject" "Multipart email"
|Set-address-header "From" "sender@example.com" "Sender"
|Set-address-header "To" "recipient@example.com" "Recipient"

; Set plain text as primary body
|Set-body "text/plain" "Plain text version for simple clients."

; Add HTML as alternative
|Add-alternative "text/html" "<p>HTML version with <b>formatting</b>.</p>"

Adding attachments

email-message :msg
|Set-header "Subject" "Email with attachment"
|Set-header 'To "recipient@example.com"
|Set-body "text/plain" "Please see the attached file."

; Attach a file
|Attach %file://document.pdf
|Attach %file://image.jpg

Sending the email

; Create SMTP dialer with server details
dialer: new-email-dialer "smtp.gmail.com" 587 "user@gmail.com" "app-password"

; Send the message
dialer .Dial-and-send msg

Parsing email files

Parse email files (EML format) to extract content.

Basic parsing

; Parse email from file
Reader %email.eml |Parse-email :eml

; Extract fields
print Subject? eml
print Text-body? eml
print Html-body? eml
print Message-id? eml

Example script for parsing

; main.clojure - parse email file from command line arg
file: first args

Reader file file |Parse-email :eml

print "Subject: " ++ Subject? eml
print "---"
print Text-body? eml

Run with: clojure main.clojure myemail.eml

Reading email (IMAP)

Rye’s IMAP support is built on github.com/emersion/go-imap/v2/imapclient. Open a connection with Open and an imap:// or imaps:// URI. The result is a native value of kind imap-client; call generic methods on it with .Method.

Connecting

; imaps:// uses implicit TLS (default port 993)
client: Open imaps://user:app-password@imap.example.com:993

; imap:// uses STARTTLS (default port 143).
; Port 993 automatically switches to implicit TLS.
client: Open imap://user:app-password@imap.example.com:993

; Keep the password out of the URI with Open\pwd
client: Open\pwd imaps://user@imap.example.com:993 "app-password"

; The path after the host selects a mailbox immediately
client: Open\pwd imaps://user@imap.example.com:993/INBOX "app-password"

; OAuth2 (XOAUTH2, with OAUTHBEARER fallback)
client: Open\oauth2 imaps://user@gmail.com@imap.gmail.com:993 "ya29.access-token"

Percent-encode reserved characters if the username or password is part of the URI. Open\pwd avoids that for passwords.

Listing folders

; Get the list of available folders
folders: client .Get-folders
folders |for { .print }
; prints: INBOX, Sent, Drafts, etc.

Selecting a folder

; Select the inbox
client .Select-folder "INBOX"

; Or other folders
client .Select-folder "Sent"
client .Select-folder "Drafts"

Searching for emails

Search-emails accepts IMAP search criteria and returns a block of matching UIDs.

; Unread messages
uids: client .Search-emails "UNSEEN"

; Combine criteria (multiple criteria are ANDed)
uids: client .Search-emails "FROM \"sender@example.com\" SUBJECT \"invoice\""

; Date searches use IMAP date format: DD-Mon-YYYY
uids: client .Search-emails "SINCE 01-Jan-2024"
uids: client .Search-emails "BEFORE 31-Dec-2024"

; Other common criteria
uids: client .Search-emails "NOT DELETED"
uids: client .Search-emails "LARGER 1048576"
uids: client .Search-emails "HEADER X-Mailer \"MyApp\""

; OR takes two search keys
uids: client .Search-emails "OR FROM \"alice@example.com\" FROM \"bob@example.com\""

Supported criteria include ALL, SEEN/UNSEEN, FLAGGED/UNFLAGGED, ANSWERED, DELETED, DRAFT, RECENT, NEW, OLD, FROM, TO, CC, BCC, SUBJECT, HEADER, BODY, TEXT, SINCE, BEFORE, ON, SENTSINCE, SENTBEFORE, SENTON, LARGER, SMALLER, UID, KEYWORD, UNKEYWORD, NOT, OR and parentheses.

Getting email overviews (fast)

Get headers only - useful when listing many messages:

uids: client .Search-emails "UNSEEN"
overviews: client .Get-overviews uids

; Each overview is a dict with: uid, subject, from, to, cc, bcc,
; date, received, message-id, size and flags
overviews |for { ::email
    subject: email -> "subject"
    from: email -> "from"
    print "Subject: " ++ subject
    print "From: " ++ from
    print "---"
}

Getting full emails

Get complete email content including bodies and attachments:

uids: client .Search-emails "UNSEEN"
emails: client .Get-emails uids

; Each email dict also has: sender, reply-to, text, html and attachments
emails |for { ::email
    subject: email -> "subject"
    from: email -> "from"
    text: email -> "text"

    print "Subject: " ++ subject
    print "From: " ++ from
    print "Text: " ++ text
    print "---"

    email -> "attachments" |for { ::attachment
        name: attachment -> "name"
        print "Attachment: " ++ name
    }
}

Get-emails fetches messages with BODY.PEEK[], so it does not mark them as read. Use Mark-seen when you want to mark a message as read.

Each attachment dict contains name, mime-type, size and content. The content is exposed as a string because Rye has no separate binary type.

Mailbox info and raw messages

Mailbox-info returns information about the currently selected folder:

client .Select-folder "INBOX"
info: client .Mailbox-info
count: info -> "messages"
print join { "Messages in INBOX: " string count }

Get-raw-emails returns the complete RFC 822 source for each message. This is useful for backups, MBOX export or feeding another parser/indexer:

uids: client .Search-emails "ALL"
raw-emails: client .Get-raw-emails uids

var 'uid 0
var 'raw ""
raw-emails |for { ::email
    uid:: email -> "uid"
    raw:: email -> "raw"
    print join { "UID " string uid ": " string length? raw " bytes" }
}

For large mailboxes, fetch UIDs in chunks with split\every to avoid loading everything into memory at once.

Managing emails

; Mark email as read
client .Mark-seen 12345

; Move email to another folder
client .Move-email 12345 "Archive"

; Delete email (marks it for deletion)
client .Delete-email 12345

; Permanently remove deleted emails
client .Expunge

Closing the connection

client .Close

Complete IMAP example

; Connect and process unread emails
client: Open\pwd imaps://user@imap.example.com:993 "app-password"

client .Select-folder "INBOX"
uids: client .Search-emails "UNSEEN"

either ( length? uids ) > 0 {
    print join { "Found " string length? uids " unread emails" }

    emails: client .Get-emails uids
    emails |for { ::email
        subject: email -> "subject"
        from: email -> "from"

        print "Subject: " ++ subject
        print "From: " ++ from
        print "---"

        ; Mark as read after processing
        client .Mark-seen email -> "uid"
    }
} {
    print "No unread emails"
}

client .Close

The older positional constructor imap-client "user" "password" "imap.example.com" 993 is still available for compatibility, but the URI-based Open form above is preferred.